Skip to content
~/sph.sh

AWS CDK Functional Patterns: Tekrar Kullanılabilir, Hatasız Infrastructure Konfigürasyonları

Functional programming pattern'leri - factory function'lar, higher-order function'lar ve composition - ile AWS CDK'yı CloudFormation generator'dan type-safe, tekrar kullanılabilir bir infrastructure toolkit'e dönüştürmeyi öğren.

Özet

AWS CDK, infrastructure'ı gerçek kod olarak ele almamızı sağlar, ancak doğru pattern'ler olmadan takımlar genellikle duplicate konfigürasyonlar, tutarsız ayarlar ve compile time'da önlenebilecek runtime hataları ile karşılaşır. Functional programming pattern'leri - higher-order function'lar, factory pattern'ler ve composition - CDK'yı CloudFormation generator'dan type-safe, tekrar kullanılabilir bir infrastructure toolkit'e dönüştürür. Bu yazıda, ortak konfigürasyonları (NodejsFunction ayarları, RemovalPolicy enforcement, logging standartları gibi) nasıl merkezileştirebileceğini ve manuel tekrar veya runtime sürprizleri olmadan tüm kaynaklarda nasıl tutarlı şekilde uygulayabileceğini göstereceğim.

İlgili yazılar: Bu yazı TypeScript'te creational pattern'ler ve builder pattern'ler üzerine inşa ediliyor ve bunları özellikle AWS CDK infrastructure'ına uyguluyor. Environment management ve migration context için Serverless'ten CDK'ya migration Bölüm 4'e göz at.

Configuration Drift Problemi

Birden fazla microservice'de AWS CDK ile çalışırken tekrarlayan bir pattern fark ettim: farklı developer'lar Lambda function'larını farklı şekillerde konfigüre ediyor. Bazıları log retention ayarlamayı unutuyor, diğerleri tutarsız timeout değerleri kullanıyor ve memory size'lar benzer workload'lar arasında rastgele değişiyor. Production veritabanları RemovalPolicy ayarlanmadığı için yanlışlıkla siliniyor, development veritabanları ise süresiz olarak korunup maliyetleri artırıyordu.

Temel sorun bilgi eksikliği değildi - enforcement eksikliğiydi. 50 satırlık Lambda konfigürasyonlarını 20+ function arasında copy-paste yapmak maintenance kabusu yaratıyordu. Gereksinimler değiştiğinde, bunları tutarlı şekilde güncellemek proje çapında bir refactoring egzersizi haline geliyordu.

Merkezi NodejsFunction Konfigürasyon Factory

Önemli bir fark yaratan en basit pattern, Lambda konfigürasyonları için bir factory function oluşturmaktı.

Pattern Olmadan:

typescript
// Her Lambda stack'te tekrarlanan kodnew NodejsFunction(this, 'UserHandler', {  runtime: Runtime.NODEJS_20_X,  handler: 'handler',  entry: 'src/handlers/user.ts',  timeout: Duration.seconds(30),  memorySize: 1024,  logRetention: RetentionDays.ONE_WEEK,  tracing: Tracing.ACTIVE,  environment: {    NODE_OPTIONS: '--enable-source-maps',    LOG_LEVEL: 'info'  },  bundling: {    minify: true,    sourceMap: true,    externalModules: ['@aws-sdk/*'],    mainFields: ['module', 'main']  }});
// Aynı config OrderHandler, ProductHandler vb. için duplicate edilmiş

Factory Pattern ile:

typescript
// lib/constructs/lambda-factory.tsexport interface LambdaConfig {  entry: string;  handler?: string;  environment?: Record<string, string>;  timeout?: Duration;  memorySize?: number;}
export function createApiLambda(  scope: Construct,  id: string,  config: LambdaConfig): NodejsFunction {  return new NodejsFunction(scope, id, {    runtime: Runtime.NODEJS_20_X,    handler: config.handler ?? 'handler',    entry: config.entry,    timeout: config.timeout ?? Duration.seconds(30),    memorySize: config.memorySize ?? 1024,    logRetention: RetentionDays.ONE_WEEK,    tracing: Tracing.ACTIVE,    environment: {      NODE_OPTIONS: '--enable-source-maps',      LOG_LEVEL: process.env.STAGE === 'prod' ? 'warn' : 'debug',      ...config.environment    },    bundling: {      minify: true,      sourceMap: true,      externalModules: ['@aws-sdk/*'],      mainFields: ['module', 'main']    }  });}
// Kullanım - temiz ve tutarlıconst userHandler = createApiLambda(this, 'UserHandler', {  entry: 'src/handlers/user.ts',  environment: { TABLE_NAME: userTable.tableName }});

8 servis genelinde 40+ Lambda function bulunan bir microservices mimarisinde, bu pattern değerini AWS Node.js 20 runtime'ını yayınladığında kanıtladı. Merkezi factory'yi güncellemek 5 dakika sürdü, 40+ bireysel function tanımını güncellemek yerine.

Higher-Order Function'lar ile RemovalPolicy Enforcement

Production veritabanlarının yanlışlıkla silinmesi teorik bir problem değil - gerçekten oluyor. Environment'a özgü policy'ler otomatik olmalı, her kaynak için tekrarlanan manuel kararlar olmamalı.

Pattern Olmadan:

typescript
// Unutulması kolay, takım genelinde tutarsızconst userTable = new Table(this, 'UserTable', {  partitionKey: { name: 'id', type: AttributeType.STRING },  billingMode: BillingMode.PAY_PER_REQUEST,  removalPolicy: RemovalPolicy.RETAIN // Manuel ayarlanmış, belki unutulmuş});
const sessionTable = new Table(this, 'SessionTable', {  partitionKey: { name: 'sessionId', type: AttributeType.STRING },  billingMode: BillingMode.PAY_PER_REQUEST  // RemovalPolicy unutulmuş - CloudFormation davranışına göre default});

Higher-Order Function ile:

typescript
// lib/utils/removal-policy.tsexport function withRemovalPolicy<T extends Construct>(  construct: T,  environment: string): T {  const policy = environment === 'prod'    ? RemovalPolicy.RETAIN    : RemovalPolicy.DESTROY;
  if (construct instanceof Table) {    construct.applyRemovalPolicy(policy);  } else if (construct instanceof Bucket) {    construct.applyRemovalPolicy(policy);  } else if (construct instanceof FileSystem) {    construct.applyRemovalPolicy(policy);  }
  return construct;}
// Kullanım - policy environment'a göre otomatikconst userTable = withRemovalPolicy(  new Table(this, 'UserTable', {    partitionKey: { name: 'id', type: AttributeType.STRING },    billingMode: BillingMode.PAY_PER_REQUEST  }),  this.stage // 'dev' veya 'prod');

Daha İyi - CDK Aspects Kullanarak:

typescript
// TÜM stateful kaynaklar için otomatik uygulaexport class RemovalPolicyAspect implements IAspect {  constructor(private readonly environment: string) {}
  visit(node: IConstruct): void {    const policy = this.environment === 'prod'      ? RemovalPolicy.RETAIN      : RemovalPolicy.DESTROY;
    if (node instanceof CfnTable) {      node.applyRemovalPolicy(policy);    } else if (node instanceof CfnBucket) {      node.applyRemovalPolicy(policy);    } else if (node instanceof CfnDBCluster) {      node.applyRemovalPolicy(policy);    }  }}
// Stack genelinde uygulaAspects.of(this).add(new RemovalPolicyAspect(this.stage));

CDK Aspects, bireysel kaynak tanımlarını değiştirmeden bir stack'teki tüm kaynaklar için policy'leri enforce etmenin güçlü bir yolunu sağlar. Bu aspect, synthesis sırasında çalışır ve environment'a göre uygun RemovalPolicy'yi uygular.

Composable Configuration Builder'lar

Lambda function'ları genellikle farklı feature kombinasyonlarına ihtiyaç duyar: bazılarının VPC erişimine, bazılarının layer'lara, bazılarının DLQ'ya, bazılarının ise hepsine ihtiyacı vardır. Bu kombinasyonları temiz şekilde konfigüre etmek composition yaklaşımı gerektirir.

typescript
// lib/constructs/lambda-composers.tsexport type LambdaComposer = (fn: NodejsFunction) => void;
export const withVpc = (vpc: IVpc, subnets: SubnetSelection): LambdaComposer =>  (fn) => {    // VPC konfigürasyonu ekle    // Not: gerçek implementasyon VPC props ile yeniden oluşturmayı gerektirir  };
export const withDLQ = (queue?: IQueue): LambdaComposer =>  (fn) => {    const dlq = queue ?? new Queue(fn, 'DLQ', {      retentionPeriod: Duration.days(14)    });    fn.addEnvironment('DLQ_URL', dlq.queueUrl);  };
export const withLayer = (layer: ILayerVersion): LambdaComposer =>  (fn) => {    fn.addLayers(layer);  };
export const withAlarm = (  errorThreshold: number = 10): LambdaComposer =>  (fn) => {    new Alarm(fn, 'ErrorAlarm', {      metric: fn.metricErrors(),      threshold: errorThreshold,      evaluationPeriods: 2    });  };
// Birden fazla davranışı compose etexport function composeLambda(  fn: NodejsFunction,  ...composers: LambdaComposer[]): NodejsFunction {  composers.forEach(composer => composer(fn));  return fn;}
// Kullanım - temiz compositionconst apiHandler = composeLambda(  createApiLambda(this, 'ApiHandler', {    entry: 'src/handlers/api.ts'  }),  withDLQ(),  withLayer(sharedLayer),  withAlarm(5));

Bu pattern, karmaşık Lambda konfigürasyonlarını basit, tekrar kullanılabilir parçalardan oluşturmana olanak tanır. Her composer function bir cross-cutting concern'i ele alır ve gerektiği gibi combine edilebilir.

Type-Safe Environment Konfigürasyonu

Environment'a özgü ayarların (VPC ID'ler, subnet ID'ler, domain name'ler) kod boyunca dağılmış olması, environment'lar arasında neyin değiştiğini anlamayı zorlaştırır. Strongly-typed bir konfigürasyon pattern'i bunu çözer.

typescript
// config/environment.tsimport { z } from 'zod';
const EnvironmentSchema = z.object({  stage: z.enum(['dev', 'staging', 'prod']),  account: z.string().regex(/^\d{12}$/),  region: z.string(),  vpc: z.object({    id: z.string(),    privateSubnetIds: z.array(z.string()).min(2),    publicSubnetIds: z.array(z.string()).min(2)  }),  domain: z.string(),  logRetention: z.number().int().positive(),  lambdaDefaults: z.object({    timeout: z.number().int().min(3).max(900),    memorySize: z.number().int().min(128).max(10240)  }),  monitoring: z.object({    enableXRay: z.boolean(),    enableDetailedMetrics: z.boolean()  })});
export type EnvironmentConfig = z.infer<typeof EnvironmentSchema>;
// config/dev.tsexport const devConfig: EnvironmentConfig = {  stage: 'dev',  account: '123456789012',  region: 'us-east-1',  vpc: {    id: 'vpc-dev123',    privateSubnetIds: ['subnet-dev1', 'subnet-dev2'],    publicSubnetIds: ['subnet-pub1', 'subnet-pub2']  },  domain: 'dev.example.com',  logRetention: 7, // günler  lambdaDefaults: {    timeout: 30,    memorySize: 512  },  monitoring: {    enableXRay: false,    enableDetailedMetrics: false  }};
// config/prod.tsexport const prodConfig: EnvironmentConfig = {  stage: 'prod',  account: '210987654321',  region: 'us-east-1',  vpc: {    id: 'vpc-prod456',    privateSubnetIds: ['subnet-prod1', 'subnet-prod2', 'subnet-prod3'],    publicSubnetIds: ['subnet-pub1', 'subnet-pub2', 'subnet-pub3']  },  domain: 'api.example.com',  logRetention: 90,  lambdaDefaults: {    timeout: 60,    memorySize: 1024  },  monitoring: {    enableXRay: true,    enableDetailedMetrics: true  }};
// config/index.tsexport function getConfig(stage: string): EnvironmentConfig {  const configs = { dev: devConfig, staging: stagingConfig, prod: prodConfig };  const config = configs[stage as keyof typeof configs];
  if (!config) {    throw new Error(`Bilinmeyen stage: ${stage}`);  }
  return EnvironmentSchema.parse(config); // Runtime validation}
// Stack'te kullanımexport class ApiStack extends Stack {  constructor(scope: Construct, id: string, config: EnvironmentConfig) {    super(scope, id, {      env: {        account: config.account,        region: config.region      }    });
    const vpc = Vpc.fromLookup(this, 'Vpc', { vpcId: config.vpc.id });
    const handler = createApiLambda(this, 'Handler', {      entry: 'src/handlers/api.ts',      timeout: Duration.seconds(config.lambdaDefaults.timeout),      memorySize: config.lambdaDefaults.memorySize    });
    if (config.monitoring.enableXRay) {      handler.addToRolePolicy(xrayPolicy);    }  }}

Zod validation, konfigürasyon hatalarını runtime'da (synth sırasında) yakalar ve geçersiz deployment'ları önler. TypeScript, compile-time type safety ve mükemmel IDE autocomplete desteği sağlar.

Sensible Default'lar ile Custom L3 Construct'lar

Her API endpoint için Lambda + API Gateway + DynamoDB table + CloudWatch alarm'ları gerektiğinde çok fazla tekrarlayan kod ortaya çıkar. Custom L3 construct'lar bu pattern'leri kapsüller.

typescript
// lib/constructs/api-endpoint.tsexport interface ApiEndpointProps {  readonly handlerEntry: string;  readonly tableName: string;  readonly partitionKey: Attribute;  readonly sortKey?: Attribute;  readonly environment?: Record<string, string>;  readonly timeout?: Duration;  readonly memorySize?: number;}
export class ApiEndpoint extends Construct {  public readonly handler: NodejsFunction;  public readonly table: Table;  public readonly api: RestApi;
  constructor(scope: Construct, id: string, props: ApiEndpointProps) {    super(scope, id);
    // Best practice'lerle DynamoDB table oluştur    this.table = new Table(this, 'Table', {      tableName: props.tableName,      partitionKey: props.partitionKey,      sortKey: props.sortKey,      billingMode: BillingMode.PAY_PER_REQUEST,      encryption: TableEncryption.AWS_MANAGED,      pointInTimeRecovery: true,      removalPolicy: RemovalPolicy.RETAIN,      stream: StreamViewType.NEW_AND_OLD_IMAGES    });
    // Standart ayarlarla Lambda oluştur    this.handler = createApiLambda(this, 'Handler', {      entry: props.handlerEntry,      timeout: props.timeout,      memorySize: props.memorySize,      environment: {        TABLE_NAME: this.table.tableName,        ...props.environment      }    });
    // Permission'ları ver    this.table.grantReadWriteData(this.handler);
    // API Gateway oluştur    this.api = new RestApi(this, 'Api', {      restApiName: `${id}-api`,      deployOptions: {        stageName: 'v1',        tracingEnabled: true,        loggingLevel: MethodLoggingLevel.INFO,        metricsEnabled: true      }    });
    const integration = new LambdaIntegration(this.handler);    this.api.root.addMethod('ANY', integration);
    // Monitoring ekle    new Alarm(this, 'ErrorAlarm', {      metric: this.handler.metricErrors(),      threshold: 5,      evaluationPeriods: 2,      alarmDescription: `${id} için hatalar`    });
    new Alarm(this, 'ThrottleAlarm', {      metric: this.handler.metricThrottles(),      threshold: 1,      evaluationPeriods: 1,      alarmDescription: `${id} için throttle'lar`    });  }
  // Ek route'lar için helper method  public addRoute(    path: string,    method: string,    handler: IFunction  ): void {    const resource = this.api.root.resourceForPath(path);    resource.addMethod(method, new LambdaIntegration(handler));  }}
// Kullanım - bir satır 80+ satırın yerini alıyorconst userEndpoint = new ApiEndpoint(this, 'UserEndpoint', {  handlerEntry: 'src/handlers/user.ts',  tableName: 'users',  partitionKey: { name: 'userId', type: AttributeType.STRING }});

Bu custom construct, best practice'leri kapsüller: encryption at rest, production için point-in-time recovery, uygun IAM permission'ları, API Gateway logging ve CloudWatch alarm'ları. Yeni takım üyeleri her detayı anlamadan kullanabilir.

CDK Aspects ile Policy Enforcement

Security gereksinimleri - encryption at rest, encryption in transit, public S3 bucket yok, her şey için CloudWatch log'ları - otomatik enforcement gerektiriyor.

typescript
// lib/aspects/security-compliance.tsexport class S3EncryptionAspect implements IAspect {  visit(node: IConstruct): void {    if (node instanceof CfnBucket) {      if (!node.bucketEncryption) {        Annotations.of(node).addError(          'S3 bucket\'larda encryption aktif olmalı'        );      }    }  }}
export class PublicAccessBlockAspect implements IAspect {  visit(node: IConstruct): void {    if (node instanceof CfnBucket) {      if (!node.publicAccessBlockConfiguration) {        node.publicAccessBlockConfiguration = {          blockPublicAcls: true,          blockPublicPolicy: true,          ignorePublicAcls: true,          restrictPublicBuckets: true        };      }    }  }}
export class LambdaLogRetentionAspect implements IAspect {  constructor(private readonly retentionDays: RetentionDays) {}
  visit(node: IConstruct): void {    if (node instanceof NodejsFunction || node instanceof Function) {      const cfnFunction = node.node.defaultChild as CfnFunction;
      // Retention policy ile log group olduğundan emin ol      new LogGroup(node, 'LogGroup', {        logGroupName: `/aws/lambda/${cfnFunction.ref}`,        retention: this.retentionDays,        removalPolicy: RemovalPolicy.DESTROY      });    }  }}
// Stack genelinde uygulaexport class SecureStack extends Stack {  constructor(scope: Construct, id: string, props?: StackProps) {    super(scope, id, props);
    // Bu stack'teki her şey için security policy'leri enforce et    Aspects.of(this).add(new S3EncryptionAspect());    Aspects.of(this).add(new PublicAccessBlockAspect());    Aspects.of(this).add(new LambdaLogRetentionAspect(RetentionDays.ONE_WEEK));  }}

Aspect'ler, her kaynak tanımını değiştirmeden organizational policy'leri enforce etmenin güçlü bir yolunu sağlar. CDK synthesis sırasında çalışır ve kaynakları validate edebilir, modify edebilir veya annotate edebilir.

Yaygın Hatalar ve Çözümleri

Aşırı Abstraction

Basit olanlar dahil her kaynak için factory function oluşturmak, faydası olmayan abstraction overhead'i yaratır. Pattern'leri, tekrarlanan konfigürasyonlara veya karmaşık validation gereksinimlerine sahip kaynaklara seçici olarak uygula. Tek bir S3 bucket factory'ye ihtiyaç duymaz.

Ne zaman abstract etmeli:

  • Kaynak benzer config ile 3+ kez görünüyor
  • Karmaşık validation logic gerekli
  • Environment'a özgü varyasyonlar gerekli
  • Security/compliance policy'leri enforce edilmeli

Implicit Dependency'ler

Belirli kaynakların (VPC, security group'lar) var olduğunu dependency'leri explicit yapmadan varsayan factory function'lar kırılgan kod yaratır. Dependency'leri function parametreleri aracılığıyla explicit yap.

typescript
// Kötü - 'vpc' nereden geliyor?function createLambda(entry: string): NodejsFunction {  return new NodejsFunction(this, 'Fn', {    entry,    vpc, // Implicit dependency  });}
// İyi - explicit dependencyfunction createLambda(  scope: Construct,  id: string,  entry: string,  vpc: IVpc): NodejsFunction {  return new NodejsFunction(scope, id, { entry, vpc });}

Wrapper'larda Type Safety Kaybı

any type'ları veya aşırı permissive generic'ler kullanmak TypeScript'in type checking'ini yok eder. Factory layer'ları boyunca strict type'ları koru.

typescript
// Kötü - type safety kayboldufunction createResource(props: any): any {  // ...}
// İyi - type safety korundufunction createResource<T extends Construct, P>(  constructClass: new (scope: Construct, id: string, props: P) => T,  scope: Construct,  id: string,  props: P): T {  return new constructClass(scope, id, props);}

Environment'lar Arasında Configuration Drift

Dev vs prod'da farklı konfigürasyon pattern'leri kullanmak "dev'de çalışıyor" production failure'larına neden olur. Tüm environment'lar için aynı code path'i kullan, sadece konfigürasyon değerlerinde farklılık olsun.

typescript
// Aynı factory, farklı configconst config = getConfig(stage); // Type-safe config
const lambda = createApiLambda(this, 'Handler', {  entry: 'src/handler.ts',  timeout: Duration.seconds(config.lambdaDefaults.timeout),  memorySize: config.lambdaDefaults.memorySize});

Infrastructure Code'u Test Etme

Infrastructure code, application code gibi test edilmeli. Resource property'lerini verify etmek için CDK assertions library'yi kullan.

typescript
import { Template } from 'aws-cdk-lib/assertions';
test('Lambda factory doğru ayarlarla function oluşturur', () => {  const stack = new Stack();
  const fn = createApiLambda(stack, 'TestFn', {    entry: 'src/test.ts'  });
  const template = Template.fromStack(stack);
  template.hasResourceProperties('AWS::Lambda::Function', {    Runtime: 'nodejs20.x',    Timeout: 30,    MemorySize: 1024,    TracingConfig: { Mode: 'Active' }  });});

Test yapmak konfigürasyon hatalarını yakalar ve factory function'ların beklenen CloudFormation resource'larını ürettiğini validate eder.

Önemli Çıkarımlar

Merkezileştirme drift'i önler. Factory function'lar ve custom construct'lar, tüm kaynakların manuel enforcement olmadan aynı standartları takip etmesini sağlar. AWS yeni runtime'lar yayınladığında veya security gereksinimleri değiştiğinde, merkezi bir factory'yi güncellemek saatler yerine dakikalar sürer.

Type safety hataları erken yakalar. Zod validation ile birleşik TypeScript, yanlış konfigüre edilmiş kaynakların deployment'ını önler. Hataları compile time veya synth time'da yakalamak, başarısız deployment başına 5-10 dakika kazandırır ve production incident'larını önler.

Aspect'ler policy'leri otomatik enforce eder. RemovalPolicy, encryption, log retention ve diğer security policy'leri, bireysel kaynaklara dokunmadan tüm stack'lerde enforce edilebilir. Bu, security audit bulgularını ve compliance ihlallerini azaltır.

Duplication yerine composition. Higher-order function'lar ve composition pattern'leri, karmaşık konfigürasyonları basit, tekrar kullanılabilir parçalardan oluşturmana olanak tanır. Bu, tipik CDK projelerinde kod duplikasyonunu %40-60 azaltır.

Environment'a özgü config explicit olmalı. Type-safe config object'leri kullanarak konfigürasyonu koddan ayır. Bu, production incident'larına neden olan "dev'de çalışıyor ama prod'da başarısız" senaryolarını önler.

Progressive enhancement en iyi çalışır. Basit factory function'larla başla. Karmaşıklık arttıkça builder pattern'leri, custom construct'lar ve Aspect'ler ekle. İlk günden aşırı mühendislik yapmak gereksiz karmaşıklık yaratır.

Infrastructure'ı application code gibi test et. CDK construct'ları kod - bunları @aws-cdk/assertions kullanarak aynı titizlikle test et. Bu, refactoring sırasında breaking change'leri yakalar ve beklenen davranışı validate eder.

Functional pattern'ler CDK'ya doğal uyar. Higher-order function'lar, composition ve immutability, CDK'nın declarative doğasıyla iyi uyumludur. Bu pattern'ler, infrastructure code'u daha maintainable ve reason about edilebilir hale getirir.

İlgili Yazılar