Skip to content
~/sph.sh

SNS/SQS Cross-Account Fan-Out: AWS'de Multi-Account Event Dağıtımı

Amazon SNS ve SQS kullanarak güvenli cross-account event dağıtımı nasıl yapılır öğrenin. IAM policy'leri, KMS şifreleme, AWS CDK implementasyonu ve production'da karşılaşılan yaygın sorunları kapsıyor.

Özet

Cross-account SNS/SQS fan-out, AWS hesap sınırları arasında güvenli event dağıtımı sağlıyor. Bu mimari pattern, bir hesaptaki tek bir SNS topic'in, farklı hesaplardaki birden fazla SQS queue'ya mesaj göndermesini sağlarken, yönetimsel izolasyonu koruyarak event-driven iletişimi mümkün kılıyor. Bu rehber, IAM policy'leri, KMS şifreleme, AWS CDK kurulumu ve production'da ortaya çıkan yaygın sorunların troubleshooting'ini içeriyor.

Cross-Account Fan-Out Neden Önemli

Multi-account AWS Organizations ile çalışmak bana düzgün event dağıtımının organizasyonel ölçek için ne kadar önemli olduğunu gösterdi. Farklı ekipler, servisler veya environment'lar için ayrı hesaplarınız varsa, güvenlik sınırlarından ödün vermeden event'leri paylaşmanın bir yoluna ihtiyacınız oluyor.

SNS/SQS fan-out pattern'i birkaç gerçek problemi çözüyor:

Yönetimsel izolasyon: Her hesap kendi kaynaklarını bağımsız olarak kontrol ediyor. Billing ekibi, fulfillment ekibinin altyapısını yanlışlıkla silemez - her ikisi de aynı kaynaktan event alsalar bile.

Bağımsız scaling: Consumer hesaplar SQS processing'lerini bağımsız olarak scale ediyorlar. Bir yavaş consumer diğerlerini etkilemiyor - mesajlar kendi hesaplarında kuyruklanırken diğerleri işlemeye devam ediyor.

Maliyet verimliliği: SNS'den SQS'e delivery ücretsiz (sadece SNS publish'ler ve SQS operasyonları için ödeme yapıyorsun). HTTP endpoint'ler veya diğer entegrasyon metodlarıyla karşılaştırıldığında, bu scale'de önemli maliyet tasarrufu sağlıyor.

Güvenlik sınırları: Her hesap kendi şifreleme, access policy'leri ve compliance kontrollerini implement ediyor. Security ekibi kendi hesabında sıkı key management uygulayabilir, publisher'da değişiklik gerektirmeden.

Mimari Genel Bakış

Cross-account fan-out pattern'i nasıl çalışıyor:

Pattern üç seviyede doğru konfigürasyon gerektiriyor:

  1. SNS topic policy: Cross-account sns:Subscribe izni veriyor
  2. SQS queue policy: SNS service principal'ının sqs:SendMessage yapmasına izin veriyor
  3. KMS key policy (şifreli ise): SNS'in mesajları şifrelemesine/çözmesine izin veriyor

IAM Policy'leri ve İzinler

Cross-account izinleri doğru yapmak kritik. Güvenilir şekilde çalışan yaklaşımı paylaşıyorum.

SNS Topic Policy (Publisher Account)

SNS topic açıkça hedef hesaplara sns:Subscribe izni vermelidir:

typescript
import * as sns from 'aws-cdk-lib/aws-sns';import * as iam from 'aws-cdk-lib/aws-iam';import { Stack, StackProps } from 'aws-cdk-lib';import { Construct } from 'constructs';
export class PublisherStack extends Stack {  public readonly topic: sns.Topic;
  constructor(scope: Construct, id: string, props?: StackProps) {    super(scope, id, props);
    // SNS topic oluştur    this.topic = new sns.Topic(this, 'CentralEventTopic', {      topicName: 'central-events',      displayName: 'Central Event Distribution Topic',    });
    // Cross-account subscribe izinleri ver    this.topic.addToResourcePolicy(      new iam.PolicyStatement({        sid: 'AllowCrossAccountSubscribe',        effect: iam.Effect.ALLOW,        principals: [          new iam.AccountPrincipal('111111111111'), // Account A          new iam.AccountPrincipal('222222222222'), // Account B          new iam.AccountPrincipal('333333333333'), // Account C        ],        actions: ['sns:Subscribe'],        resources: [this.topic.topicArn],      })    );
    // İsteğe bağlı olarak tüm hesap yerine belirli IAM role'lere izin ver    // Bu daha kısıtlayıcı ve least-privilege prensibini takip ediyor    this.topic.addToResourcePolicy(      new iam.PolicyStatement({        sid: 'AllowSpecificRoleSubscribe',        effect: iam.Effect.ALLOW,        principals: [          new iam.ArnPrincipal('arn:aws:iam::111111111111:role/ServiceARole'),        ],        actions: ['sns:Subscribe'],        resources: [this.topic.topicArn],      })    );  }}

Önemli noktalar:

  • Organization-wide erişim için AccountPrincipal, spesifik roller için ArnPrincipal kullan
  • sns:Subscribe action'ı subscription oluşturmak için gerekli
  • Bu policy mesaj yayınlama izni vermiyor - sadece subscription oluşturma
  • Kaynak VPC, IP aralığı veya diğer faktörlere göre kısıtlamak için condition'lar ekleyebilirsin

SQS Queue Policy (Consumer Account)

Her consumer hesap, SNS service principal'ının mesaj göndermesine izin veren bir queue policy'sine ihtiyaç duyuyor:

typescript
import * as sqs from 'aws-cdk-lib/aws-sqs';import * as sns from 'aws-cdk-lib/aws-sns';import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';import * as iam from 'aws-cdk-lib/aws-iam';import { Duration, Stack, StackProps } from 'aws-cdk-lib';import { Construct } from 'constructs';
interface ConsumerStackProps extends StackProps {  centralTopicArn: string; // Publisher account'tan ARN}
export class ConsumerStack extends Stack {  constructor(scope: Construct, id: string, props: ConsumerStackProps) {    super(scope, id, props);
    // Başarısız mesajlar için dead letter queue oluştur    const dlq = new sqs.Queue(this, 'EventDLQ', {      queueName: 'events-dlq',      retentionPeriod: Duration.days(14),    });
    // Ana event queue'yu oluştur    const queue = new sqs.Queue(this, 'EventQueue', {      queueName: 'service-events',      visibilityTimeout: Duration.seconds(30),      receiveMessageWaitTime: Duration.seconds(20), // Long polling aktif et      deadLetterQueue: {        queue: dlq,        maxReceiveCount: 3,      },    });
    // SNS'in mesaj göndermesine izin veren queue policy ekle    queue.addToResourcePolicy(      new iam.PolicyStatement({        sid: 'AllowSNSPublish',        effect: iam.Effect.ALLOW,        principals: [new iam.ServicePrincipal('sns.amazonaws.com')],        actions: ['sqs:SendMessage'],        resources: [queue.queueArn],        conditions: {          ArnEquals: {            'aws:SourceArn': props.centralTopicArn,          },        },      })    );
    // Cross-account SNS topic'i import et    const centralTopic = sns.Topic.fromTopicArn(      this,      'CentralTopic',      props.centralTopicArn    );
    // Queue'yu topic'e subscribe et    centralTopic.addSubscription(      new subscriptions.SqsSubscription(queue, {        rawMessageDelivery: false, // Sadece mesaj body'si almak için true yap      })    );  }}

Önemli detaylar:

  • aws:SourceArn ile Condition diğer SNS topic'lerin queue'na göndermesini engelliyor
  • rawMessageDelivery: false mesajı SNS metadata'sıyla sarıyor (debugging için öneriliyor)
  • rawMessageDelivery: true yap eğer sadece SNS envelope olmadan mesaj body'si istiyorsan
  • Long polling (receiveMessageWaitTime) boş receive'leri ve maliyeti azaltıyor

İki Yönlü El Sıkışma

Cross-account subscription'lar her iki hesabın da anlaşmasını gerektiriyor:

  1. Publisher subscription'a izin veriyor: SNS topic policy consumer hesaba sns:Subscribe veriyor
  2. Consumer mesajları kabul ediyor: SQS queue policy SNS service principal'ının mesaj göndermesine izin veriyor
  3. Consumer subscription oluşturuyor: Queue owner topic ARN kullanarak sns:Subscribe çağırıyor

Bu iki yönlü el sıkışma kritik. Eğer policy'lerden biri eksikse, "Access Denied" hataları alırsın. Subscription başarısızlıklarında troubleshooting yaparken her iki tarafı da kontrol etmeyi öğrendim.

KMS Şifreleme Konfigürasyonu

Şifreleme cross-account setup'lara karmaşıklık ekliyor. AWS-managed key'ler hesap sınırları arasında çalışmıyor - customer-managed key kullanmalısın.

AWS-Managed Key'ler Neden Çalışmıyor

AWS-managed key (alias/aws/sqs) kullanarak şifreli bir SQS queue oluşturduğunda, key policy sadece o hesap içinde izinler veriyor. Publisher hesaptaki SNS servisi, consumer hesabın AWS-managed key'ini kullanamıyor.

Customer-Managed Key Kurulumu

Şifreli queue'lar için çalışan bir pattern:

typescript
import * as kms from 'aws-cdk-lib/aws-kms';import * as sqs from 'aws-cdk-lib/aws-sqs';import * as sns from 'aws-cdk-lib/aws-sns';import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';import * as iam from 'aws-cdk-lib/aws-iam';import * as cdk from 'aws-cdk-lib';import { Duration, Stack, StackProps } from 'aws-cdk-lib';import { Construct } from 'constructs';
interface EncryptedConsumerStackProps extends StackProps {  centralTopicArn: string;}
export class EncryptedConsumerStack extends Stack {  constructor(scope: Construct, id: string, props: EncryptedConsumerStackProps) {    super(scope, id, props);
    // Customer-managed KMS key oluştur    const queueKey = new kms.Key(this, 'QueueEncryptionKey', {      description: 'KMS key for cross-account SQS queue encryption',      enableKeyRotation: true,      removalPolicy: cdk.RemovalPolicy.RETAIN, // Key'leri silme    });
    // SNS servisine key kullanma izni ver    queueKey.addToResourcePolicy(      new iam.PolicyStatement({        sid: 'AllowSNSToUseKey',        effect: iam.Effect.ALLOW,        principals: [new iam.ServicePrincipal('sns.amazonaws.com')],        actions: [          'kms:Decrypt',          'kms:GenerateDataKey',        ],        resources: ['*'],        conditions: {          StringEquals: {            // SNS'in bu key'i sadece bu region'daki SQS için kullanmasını sağla            'kms:ViaService': `sqs.${this.region}.amazonaws.com`,          },        },      })    );
    // Şifreli dead letter queue oluştur    const dlq = new sqs.Queue(this, 'EncryptedEventDLQ', {      queueName: 'encrypted-events-dlq',      encryptionMasterKey: queueKey,      retentionPeriod: Duration.days(14),    });
    // Şifreli ana queue oluştur    const queue = new sqs.Queue(this, 'EncryptedEventQueue', {      queueName: 'encrypted-service-events',      encryptionMasterKey: queueKey,      visibilityTimeout: Duration.seconds(30),      receiveMessageWaitTime: Duration.seconds(20),      deadLetterQueue: {        queue: dlq,        maxReceiveCount: 3,      },    });
    // SNS'in mesaj göndermesine izin veren queue policy    queue.addToResourcePolicy(      new iam.PolicyStatement({        sid: 'AllowSNSPublish',        effect: iam.Effect.ALLOW,        principals: [new iam.ServicePrincipal('sns.amazonaws.com')],        actions: ['sqs:SendMessage'],        resources: [queue.queueArn],        conditions: {          ArnEquals: {            'aws:SourceArn': props.centralTopicArn,          },        },      })    );
    // Cross-account topic'i import et ve subscribe ol    const centralTopic = sns.Topic.fromTopicArn(      this,      'CentralTopic',      props.centralTopicArn    );
    centralTopic.addSubscription(      new subscriptions.SqsSubscription(queue)    );  }}

Key policy gereksinimleri:

  • kms:Decrypt: SNS queue'ya gönderirken mesajları decrypt etmek için buna ihtiyaç duyuyor
  • kms:GenerateDataKey: Envelope encryption için gerekli
  • kms:ViaService condition: Key kullanımını belirli region'daki SQS servisine kısıtlıyor
  • Güvenlik best practice'leri için key rotation'ı aktif et

Maliyet Değerlendirmesi

Customer-managed KMS key'ler key başına ayda 1a,artı10.000istekbas\cına1'a, artı 10.000 istek başına 0.03'e mal oluyor. Cross-account senaryolar için bu gerekli - ücretsiz alternatif yok.

Maliyet Optimizasyonu için Mesaj Filtreleme

SNS subscription filter'ları istenmeyen mesajların queue'lara ulaşmasını önleyerek maliyetleri azaltıyor. Filtreleme SQS ücretleri uygulanmadan önce SNS seviyesinde gerçekleşiyor.

Attribute-Based Filtreleme

Mesaj attribute'ları basit, verimli filtreleme sağlıyor:

typescript
// Publisher: Attribute'larla yayınlaimport { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
const sns = new SNSClient({ region: 'us-east-1' });
await sns.send(  new PublishCommand({    TopicArn: 'arn:aws:sns:us-east-1:999999999999:central-events',    Message: JSON.stringify({      orderId: '12345',      amount: 1500,      region: 'us-east-1',    }),    MessageAttributes: {      eventType: {        DataType: 'String',        StringValue: 'OrderCreated',      },      priority: {        DataType: 'String',        StringValue: 'high',      },      amount: {        DataType: 'Number',        StringValue: '1500',      },      region: {        DataType: 'String',        StringValue: 'us-east-1',      },    },  }));
typescript
// Consumer: Filter policy ile subscribe olimport * as sns from 'aws-cdk-lib/aws-sns';import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
centralTopic.addSubscription(  new subscriptions.SqsSubscription(highPriorityQueue, {    filterPolicy: {      // Sadece yüksek öncelikli event'ler      priority: sns.SubscriptionFilter.stringFilter({        allowlist: ['high', 'critical'],      }),      // Sadece büyük siparişler      amount: sns.SubscriptionFilter.numericFilter({        greaterThan: 1000,      }),    },  }));
centralTopic.addSubscription(  new subscriptions.SqsSubscription(regionalQueue, {    filterPolicy: {      // Sadece belirli region'lar      region: sns.SubscriptionFilter.stringFilter({        allowlist: ['us-east-1', 'eu-west-1'],      }),    },  }));
// Analytics queue her şeyi alıyor (filter yok)centralTopic.addSubscription(  new subscriptions.SqsSubscription(analyticsQueue));

Payload-Based Filtreleme

Daha yeni payload-based filtreleme (2024'te tanıtıldı) mesaj body'sinin kendisinde filtrelemeye izin veriyor:

typescript
import { SNSClient, SubscribeCommand } from '@aws-sdk/client-sns';
const sns = new SNSClient({ region: 'us-east-1' });
await sns.send(  new SubscribeCommand({    TopicArn: 'arn:aws:sns:us-east-1:999999999999:central-events',    Protocol: 'sqs',    Endpoint: 'arn:aws:sqs:us-east-1:111111111111:service-events',    Attributes: {      FilterPolicyScope: 'MessageBody',      FilterPolicy: JSON.stringify({        order: {          status: ['completed', 'shipped'],          amount: [{ numeric: ['>', 1000] }],        },      }),    },  }));

Filter policy faydaları:

  • Tipik senaryolarda SQS istek maliyetlerini %50-90 azaltıyor
  • Her subscriber sadece ilgili mesajları alıyor
  • Filter değişiklikleri yayılması 15 dakika kadar sürebiliyor
  • Topic başına 200'e kadar filter policy

FIFO Topic'ler ve Queue'lar

FIFO (First-In-First-Out) topic'ler kesin sıralama ve tam bir kez delivery sağlıyor. Mesaj sırası önemli olduğunda kullan.

FIFO Ne Zaman Kullanılmalı

FIFO şu durumlarda mantıklı:

  • Sıranın önemli olduğu sipariş işleme workflow'ları
  • Tam bir kez işleme gerektiren finansal işlemler
  • Sırasıyla gerçekleşmesi gereken state machine geçişleri
  • Sıranın nihai durumu etkilediği envanter güncellemeleri

FIFO Kurulum Gereksinimleri

typescript
import * as sns from 'aws-cdk-lib/aws-sns';import * as sqs from 'aws-cdk-lib/aws-sqs';import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
// FIFO topic oluştur (publisher account)const fifoTopic = new sns.Topic(this, 'OrderEventTopic', {  topicName: 'order-events.fifo',  fifo: true,  contentBasedDeduplication: true,  // High-throughput modu: Mesaj grubu başına 3000+ TPS  fifoThroughputScope: sns.FifoThroughputScope.MESSAGE_GROUP,});
// FIFO queue oluştur (consumer account)const fifoQueue = new sqs.Queue(this, 'OrderQueue', {  queueName: 'orders.fifo',  fifo: true,  contentBasedDeduplication: true,});
// FIFO topic sadece FIFO queue'lara subscribe olabilirfifoTopic.addSubscription(  new subscriptions.SqsSubscription(fifoQueue));

FIFO topic'lere yayınlama:

typescript
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
const sns = new SNSClient({ region: 'us-east-1' });
await sns.send(  new PublishCommand({    TopicArn: 'arn:aws:sns:us-east-1:999999999999:order-events.fifo',    Message: JSON.stringify({ orderId: '12345', status: 'created' }),    MessageGroupId: 'order-region-us-east-1', // Sıralama için gerekli    MessageDeduplicationId: 'order-12345-created', // contentBasedDeduplication aktifse opsiyonel  }));

High-throughput mode değerlendirmeleri:

  • Default FIFO: Topic seviyesinde deduplication ile 300 TPS
  • High-throughput mode: Mesaj grubu seviyesinde deduplication ile 30.000 TPS (Ocak 2025'te artırıldı)
  • Aktif edildikten sonra geri alınamaz
  • Processing'i paralel hale getirmek için birden fazla mesaj grubu kullan

Yaygın Hatalar ve Çözümler

Production'da karşılaştığım sorunlar ve çözümleri.

Hata 1: Subscription "PendingConfirmation" Gösteriyor

Belirti: Subscription oluşturuldu ama "PendingConfirmation" durumunda takılı kaldı. Mesajlar akmıyor.

Kök neden: Topic owner subscription'ı oluşturduğunda (queue owner yerine), SNS manuel olarak onaylanması gereken bir confirmation mesajı gönderiyor.

Çözüm: Her zaman queue owner'ın subscription oluşturmasını sağla:

typescript
// TERCİH EDİLEN: Queue owner subscribe oluyor (consumer account'ta)const centralTopic = sns.Topic.fromTopicArn(  this,  'CentralTopic',  'arn:aws:sns:us-east-1:999999999999:central-events');
centralTopic.addSubscription(  new subscriptions.SqsSubscription(queue));// Onay gerekmiyor - subscription hemen aktif

Eğer topic owner subscription oluşturmalıysa, onayı otomatikleştir:

python
# Subscription'ları otomatik onaylamak için Python scriptiimport boto3import json
sqs = boto3.client('sqs')queue_url = 'https://sqs.us-east-1.amazonaws.com/111111111111/service-events'
# Confirmation mesajı için poll etresponse = sqs.receive_message(    QueueUrl=queue_url,    MaxNumberOfMessages=1,    WaitTimeSeconds=10)
for message in response.get('Messages', []):    body = json.loads(message['Body'])
    if 'SubscribeURL' in body:        # URL'yi ziyaret ederek subscription'ı onayla        import urllib.request        urllib.request.urlopen(body['SubscribeURL'])        print(f"Subscription onaylandı: {body['SubscribeURL']}")
        # Confirmation mesajını sil        sqs.delete_message(            QueueUrl=queue_url,            ReceiptHandle=message['ReceiptHandle']        )

Hata 2: KMS Key Access Denied

Belirti: Mesajlar SNS'e yayınlanıyor ama şifreli SQS queue'da hiç görünmüyor. SNS metriklerinde hata yok.

Kök neden: SNS servisi KMS key'i şifreleme için kullanma iznine sahip değil.

Çözüm: KMS key policy'nin SNS'e gerekli izinleri verdiğini doğrula:

typescript
// KMS key policy'nin bunu içerdiğinden emin olqueueKey.addToResourcePolicy(  new iam.PolicyStatement({    effect: iam.Effect.ALLOW,    principals: [new iam.ServicePrincipal('sns.amazonaws.com')],    actions: [      'kms:Decrypt',      'kms:GenerateDataKey',    ],    resources: ['*'],    conditions: {      StringEquals: {        'kms:ViaService': `sqs.${this.region}.amazonaws.com`,      },    },  }));

Troubleshooting ipucu: KMS AccessDenied hataları için CloudTrail loglarını kontrol et:

bash
aws cloudtrail lookup-events \  --lookup-attributes AttributeKey=EventName,AttributeValue=Decrypt \  --max-results 50 \  --region us-east-1 \  --query 'Events[?ErrorCode==`AccessDenied`]'

Hata 3: Region Uyuşmazlığı

Belirti: Doğru policy'lere rağmen "Access Denied" hataları.

Kök neden: SNS topic farklı region'da, SQS queue başka region'da. Cross-region direct subscription'lar cross-account senaryolar için desteklenmiyor.

Çözüm: SNS topic ve SQS queue'ları aynı region'da tut. Multi-region gereksinimler için SNS'den Lambda forwarder'lar kullan:

typescript
// Region 1 (us-east-1): Orijinal topicconst sourceTopicEast = new sns.Topic(this, 'SourceTopicEast', {  topicName: 'events-us-east-1',});
// Lambda forwarder region 2 topic'ine yayınlıyorconst forwarder = new lambda.Function(this, 'RegionForwarder', {  runtime: lambda.Runtime.NODEJS_20_X,  handler: 'index.handler',  code: lambda.Code.fromInline(`    const { SNSClient, PublishCommand } = require('@aws-sdk/client-sns');    const sns = new SNSClient({ region: 'eu-west-1' });
    exports.handler = async (event) => {      for (const record of event.Records) {        await sns.send(new PublishCommand({          TopicArn: process.env.TARGET_TOPIC_ARN,          Message: record.Sns.Message,          MessageAttributes: record.Sns.MessageAttributes,        }));      }    };  `),  environment: {    TARGET_TOPIC_ARN: 'arn:aws:sns:eu-west-1:999999999999:events-eu-west-1',  },});
sourceTopicEast.addSubscription(  new subscriptions.LambdaSubscription(forwarder));

Hata 4: Mesaj Boyutu Limitleri

Belirti: Bazı mesajlar başarıyla gönderiliyor, diğerleri sessizce kayboluyor.

Kök neden: SNS ve SQS'in her ikisinde de 256 KB mesaj boyutu limiti var. Bu limiti aşan mesajlar bildirim olmadan düşürülüyor.

Çözüm: Mesajları 256 KB altında tut veya büyük payload'lar için S3 kullan:

typescript
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
const s3 = new S3Client({ region: 'us-east-1' });const sns = new SNSClient({ region: 'us-east-1' });
async function publishLargeMessage(topicArn: string, payload: any) {  const payloadSize = Buffer.byteLength(JSON.stringify(payload));
  if (payloadSize > 200_000) {    // Büyük payload'u S3'e kaydet    const messageId = crypto.randomUUID();    const s3Key = `messages/${messageId}.json`;
    await s3.send(      new PutObjectCommand({        Bucket: 'large-message-payloads',        Key: s3Key,        Body: JSON.stringify(payload),      })    );
    // S3 object'e referans yayınla    await sns.send(      new PublishCommand({        TopicArn: topicArn,        Message: JSON.stringify({          type: 'S3Reference',          bucket: 'large-message-payloads',          key: s3Key,        }),      })    );  } else {    // Küçük mesajlar için direkt yayınla    await sns.send(      new PublishCommand({        TopicArn: topicArn,        Message: JSON.stringify(payload),      })    );  }}

Hata 5: Filter Policy'ler Etkili Olmuyor

Belirti: Filter policy'ye rağmen mesajlar hala gönderiliyor.

Kök neden: Filter policy'lerin yayılması 15 dakika kadar sürebiliyor, veya mesaj attribute'ları filter formatıyla eşleşmiyor.

Çözüm: Yayılmayı bekle ve attribute formatını doğrula:

typescript
// Mesaj attribute'larının filter beklentileriyle eşleştiğini doğrulaawait sns.send(  new PublishCommand({    TopicArn: topicArn,    Message: JSON.stringify({ orderId: '12345' }),    MessageAttributes: {      eventType: {        DataType: 'String',        StringValue: 'OrderCreated', // Filter ile tam eşleşmelidir      },      priority: {        DataType: 'Number', // Numeric filter'lar için Number tipini kullan        StringValue: '5',      },    },  }));
// Filtreleme etkinliğini izleconst filterMetric = new cloudwatch.Metric({  namespace: 'AWS/SNS',  metricName: 'NumberOfNotificationsFilteredOut',  dimensionsMap: {    TopicName: 'central-events',  },});

Monitoring ve Observability

Cross-account mesajlaşma için etkili monitoring önemli. Hem publisher hem de consumer taraflarında görünürlüğe ihtiyacın var.

Önemli SNS Metrikleri

typescript
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';import * as cloudwatch_actions from 'aws-cdk-lib/aws-cloudwatch-actions';import * as sns from 'aws-cdk-lib/aws-sns';
// SNS delivery başarısızlıklarını izlenew cloudwatch.Alarm(this, 'SNSDeliveryFailures', {  metric: topic.metricNumberOfNotificationsFailed({    statistic: 'Sum',    period: Duration.minutes(5),  }),  threshold: 10,  evaluationPeriods: 2,  treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,  alarmDescription: 'SNS mesaj delivery başarısız olduğunda uyarı ver',});
// Filter etkinliğini izlenew cloudwatch.Alarm(this, 'HighFilterRate', {  metric: topic.metricNumberOfNotificationsFilteredOut({    statistic: 'Sum',    period: Duration.minutes(5),  }),  threshold: 1000,  evaluationPeriods: 1,  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,  alarmDescription: 'Filter oranı olağandışı yüksek olduğunda uyarı ver',});

Önemli SQS Metrikleri

typescript
// Queue depth'i izlenew cloudwatch.Alarm(this, 'QueueDepthAlarm', {  metric: queue.metricApproximateNumberOfMessagesVisible({    statistic: 'Average',    period: Duration.minutes(5),  }),  threshold: 1000,  evaluationPeriods: 2,  alarmDescription: 'Queue depth çok büyüdüğünde uyarı ver',});
// Processing lag'i izlenew cloudwatch.Alarm(this, 'OldMessageAlarm', {  metric: queue.metricApproximateAgeOfOldestMessage({    statistic: 'Maximum',    period: Duration.minutes(1),  }),  threshold: 300, // 5 dakika  evaluationPeriods: 3,  alarmDescription: 'Mesajlar zamanında işlenmediğinde uyarı ver',});
// DLQ'yu izlenew cloudwatch.Alarm(this, 'DLQMessages', {  metric: dlq.metricApproximateNumberOfMessagesVisible({    statistic: 'Sum',    period: Duration.minutes(5),  }),  threshold: 1,  evaluationPeriods: 1,  alarmDescription: 'DLQ\'da herhangi bir mesaj olduğunda uyarı ver',});

Cross-Account CloudWatch Observability

Hesaplar arası birleşik monitoring için CloudWatch Observability Access Manager kullan:

typescript
import * as oam from 'aws-cdk-lib/aws-oam';
// Monitoring account'ta: Sink oluşturconst sink = new oam.CfnSink(this, 'MonitoringSink', {  name: 'central-monitoring-sink',  policy: {    Version: '2012-10-17',    Statement: [      {        Effect: 'Allow',        Principal: {          AWS: [            'arn:aws:iam::111111111111:root',            'arn:aws:iam::222222222222:root',            'arn:aws:iam::333333333333:root',          ],        },        Action: ['oam:CreateLink', 'oam:UpdateLink'],        Resource: '*',      },    ],  },});
// Her source account'ta: Sink'e link oluşturconst link = new oam.CfnLink(this, 'MonitoringLink', {  resourceTypes: ['AWS::CloudWatch::Metric', 'AWS::Logs::LogGroup'],  sinkIdentifier: 'arn:aws:oam:us-east-1:999999999999:sink/sink-id',});

Bu, veri transfer maliyeti olmadan (aynı region içinde) tüm hesaplardan metrikleri gösteren tek bir dashboard sağlıyor.

Maliyet Analizi

Maliyetleri anlamak mimarini optimize etmene yardımcı oluyor.

Fiyatlandırma Dökümü (2025)

SNS maliyetleri:

  • İlk 1 milyon istek/ay: ÜCRETSİZ
  • Free tier'ın üstünde: Milyon yayın başına $0.50
  • SNS'den SQS'e delivery: ÜCRETSİZ (büyük maliyet avantajı)

SQS maliyetleri:

  • İlk 1 milyon istek/ay: ÜCRETSİZ
  • Standard queue: Milyon istek başına $0.40
  • FIFO queue: Milyon istek başına $0.50
  • Her 64 KB chunk = 1 istek (256 KB mesaj = 4 istek)

Fan-out maliyet örneği (4 queue'ya 1 milyon mesaj):

  • SNS yayınları: 1M × 0.50=0.50 = 0.50
  • SNS'den SQS'e delivery: ÜCRETSİZ
  • SQS alımları: 4M × 0.40=0.40 = 1.60
  • SQS silmeleri: 4M × 0.40=0.40 = 1.60
  • Toplam: $3.70

%50 mesaj filtreleme ile:

  • SNS yayınları: 1M × 0.50=0.50 = 0.50
  • Filtreli delivery'ler: 2M mesaj gönderildi
  • SQS alımları: 2M × 0.40=0.40 = 0.80
  • SQS silmeleri: 2M × 0.40=0.40 = 0.80
  • Toplam: $2.10 (%43 maliyet azalması)

KMS maliyetleri (şifreli queue'lar için):

  • Customer-managed key: Key başına ayda $1
  • KMS istekleri: 10.000 istek başına $0.03
  • Her şifreli mesaj 2 KMS isteği oluşturuyor

Maliyet Optimizasyon Stratejileri

  1. Mesaj filtreleme uygula: Tipik senaryolarda %50-70 maliyet azalması
  2. SQS long polling'i aktif et: Boş alımları %90 azaltıyor
  3. Batch operasyonlarını kullan: API çağrılarında 10 kata kadar azalma
  4. Mesajları 64 KB altında tut: Multi-request ücretlerinden kaçın
  5. Sıralama kritik değilse Standard queue kullan: FIFO'dan %20 daha ucuz

Alternatif Yaklaşımlar

SNS/SQS fan-out her zaman en iyi seçim değil. İşte alternatifler ve ne zaman düşünüleceği.

EventBridge

Ne zaman kullanılmalı:

  • Karmaşık event routing gerekiyor (100+ kural)
  • Schema registry ve validasyon gerekli
  • Event replay yeteneği önemli
  • 30+ AWS servisiyle entegrasyon

Değişim değerlendirmeleri:

  • Milyon event başına 1.00(SNS1.00 (SNS 0.50'ye karşı)
  • JSONPath benzeri syntax ile daha güçlü filtreleme
  • Yerleşik schema discovery ve validation
  • Native cross-account event bus'ları

Kinesis Data Streams

Ne zaman kullanılmalı:

  • Sıralı stream processing gerekiyor
  • Replay yeteneği gerekli (365 güne kadar)
  • Farklı hızlarda okuyan birden fazla consumer
  • Real-time analytics kullanım alanları

Değişim değerlendirmeleri:

  • Daha pahalı (shard saat başına $0.015 + PUT maliyetleri)
  • Karmaşık shard yönetimi
  • Discrete event'lerden çok streaming analytics için daha iyi
  • Daha yüksek operasyonel yük

Direkt Lambda Invocation

Ne zaman kullanılmalı:

  • Synchronous processing kabul edilebilir
  • Event hacmi Lambda concurrent execution limitlerinin altında
  • Queue yönetimine gerek yok
  • Basit, hızlı processing mantığı

Değişim değerlendirmeleri:

  • Yerleşik retry queue'ları yok
  • Cold start değerlendirmeleri
  • Lambda concurrency ile sınırlı
  • Scaling için queue'lardan daha az esnek

Gerçek Dünya Implementation Pattern'i

İşte production-ready, eksiksiz bir multi-account kurulum:

typescript
// publisher-stack.tsimport * as cdk from 'aws-cdk-lib';import * as sns from 'aws-cdk-lib/aws-sns';import * as iam from 'aws-cdk-lib/aws-iam';import { Construct } from 'constructs';
export class PublisherStack extends cdk.Stack {  public readonly topicArn: string;
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {    super(scope, id, props);
    const topic = new sns.Topic(this, 'CentralEvents', {      topicName: 'central-events',      displayName: 'Central Event Distribution',    });
    // Birden fazla consumer account'a izin ver    topic.addToResourcePolicy(      new iam.PolicyStatement({        sid: 'AllowConsumerSubscribe',        principals: [          new iam.AccountPrincipal('111111111111'),          new iam.AccountPrincipal('222222222222'),          new iam.AccountPrincipal('333333333333'),        ],        actions: ['sns:Subscribe'],        resources: [topic.topicArn],      })    );
    this.topicArn = topic.topicArn;
    // Cross-stack referansları için output    new cdk.CfnOutput(this, 'TopicArnOutput', {      value: topic.topicArn,      exportName: 'CentralEventTopicArn',    });  }}
// consumer-stack.tsimport * as cdk from 'aws-cdk-lib';import * as sqs from 'aws-cdk-lib/aws-sqs';import * as sns from 'aws-cdk-lib/aws-sns';import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';import * as lambda from 'aws-cdk-lib/aws-lambda';import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources';import * as iam from 'aws-cdk-lib/aws-iam';import * as kms from 'aws-cdk-lib/aws-kms';import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';import { Construct } from 'constructs';
interface ConsumerStackProps extends cdk.StackProps {  centralTopicArn: string;  serviceName: string;}
export class ConsumerStack extends cdk.Stack {  constructor(scope: Construct, id: string, props: ConsumerStackProps) {    super(scope, id, props);
    // Şifreleme için KMS key oluştur    const encryptionKey = new kms.Key(this, 'EncryptionKey', {      description: `${props.serviceName} event'leri için şifreleme key'i`,      enableKeyRotation: true,    });
    // SNS'e key kullanma izni ver    encryptionKey.addToResourcePolicy(      new iam.PolicyStatement({        principals: [new iam.ServicePrincipal('sns.amazonaws.com')],        actions: ['kms:Decrypt', 'kms:GenerateDataKey'],        resources: ['*'],        conditions: {          StringEquals: {            'kms:ViaService': `sqs.${this.region}.amazonaws.com`,          },        },      })    );
    // DLQ oluştur    const dlq = new sqs.Queue(this, 'DLQ', {      queueName: `${props.serviceName}-dlq`,      encryptionMasterKey: encryptionKey,      retentionPeriod: cdk.Duration.days(14),    });
    // Ana queue oluştur    const queue = new sqs.Queue(this, 'EventQueue', {      queueName: `${props.serviceName}-events`,      encryptionMasterKey: encryptionKey,      visibilityTimeout: cdk.Duration.seconds(30),      receiveMessageWaitTime: cdk.Duration.seconds(20),      deadLetterQueue: {        queue: dlq,        maxReceiveCount: 3,      },    });
    // SNS'in mesaj göndermesine izin ver    queue.addToResourcePolicy(      new iam.PolicyStatement({        principals: [new iam.ServicePrincipal('sns.amazonaws.com')],        actions: ['sqs:SendMessage'],        resources: [queue.queueArn],        conditions: {          ArnEquals: {            'aws:SourceArn': props.centralTopicArn,          },        },      })    );
    // Central topic'e subscribe ol    const centralTopic = sns.Topic.fromTopicArn(      this,      'CentralTopic',      props.centralTopicArn    );
    centralTopic.addSubscription(      new subscriptions.SqsSubscription(queue, {        rawMessageDelivery: false,      })    );
    // Processor Lambda oluştur    const processor = new lambda.Function(this, 'EventProcessor', {      runtime: lambda.Runtime.NODEJS_20_X,      handler: 'index.handler',      code: lambda.Code.fromInline(`        exports.handler = async (event) => {          for (const record of event.Records) {            const snsMessage = JSON.parse(record.body);            const message = JSON.parse(snsMessage.Message);
            console.log('Mesaj işleniyor:', message);
            // İş mantığın buraya
            // Handler başarılı olursa mesaj otomatik olarak siliniyor          }        };      `),      timeout: cdk.Duration.seconds(30),      environment: {        SERVICE_NAME: props.serviceName,      },    });
    // Queue'yu Lambda'ya bağla    processor.addEventSource(      new lambdaEventSources.SqsEventSource(queue, {        batchSize: 10,        reportBatchItemFailures: true,      })    );
    // CloudWatch alarmları    new cloudwatch.Alarm(this, 'QueueDepthAlarm', {      metric: queue.metricApproximateNumberOfMessagesVisible(),      threshold: 1000,      evaluationPeriods: 2,    });
    new cloudwatch.Alarm(this, 'DLQMessagesAlarm', {      metric: dlq.metricApproximateNumberOfMessagesVisible(),      threshold: 1,      evaluationPeriods: 1,    });
    new cloudwatch.Alarm(this, 'ProcessorErrorsAlarm', {      metric: processor.metricErrors(),      threshold: 10,      evaluationPeriods: 2,    });  }}

Önemli Çıkarımlar

Cross-account SNS/SQS ile çalışmak bana birkaç önemli ders verdi:

Şifreli cross-account queue'lar için her zaman customer-managed KMS key kullan. AWS-managed key'ler hesap sınırları arasında çalışmıyor. Key başına ayda $1 kaçınılmaz.

Subscription'ları queue owner oluştursun. Bu, manuel onay adımını ortadan kaldırıyor ve kurulum karmaşıklığını azaltıyor. Topic owner subscription oluşturduğunda, confirmation mesajlarını işlemek için otomasyona ihtiyacın oluyor.

Baştan comprehensive monitoring implement et. Cross-account troubleshooting düzgün CloudWatch metrikleri olmadan önemli ölçüde daha zor. Hem publisher hem de consumer hesaplarında alarm kur.

SNS seviyesinde subscription filter'larla filtrele. Bu tipik senaryolarda maliyetleri %50-90 azaltıyor. Filtreleme SQS ücretleri uygulanmadan önce gerçekleşiyor, bu da onu maliyet açısından oldukça etkili yapıyor.

SNS topic'leri ve SQS queue'ları aynı region'da tut. Cross-region subscription'lar önemli karmaşıklık ekliyor. Multi-region dağıtıma ihtiyacın varsa, Lambda forwarder'ları kullan.

DLQ'lar subscriber account'ta olmalı. Cross-account subscription'lar için publisher hesabından bir DLQ kullanamazsın. Her consumer hesabın kendi DLQ'su olmalı.

15 dakikalık filter policy yayılmasını planla. Filter policy'leri güncellerken hemen değişiklik bekleme. Değişiklikleri önce non-production'da test et.

SNS/SQS fan-out pattern'i AWS hesapları arasında güvenilir, maliyet etkin event dağıtımı sağlıyor. Bağımsız consumer scaling'i ile persistent queue'lara ve güçlü yönetimsel sınırlara ihtiyacın olduğunda, bu mimari mükemmel sonuçlar veriyor. İzin modelini ve şifreleme gereksinimlerini anladıktan sonra implementation karmaşıklığı yönetilebilir.

İlgili Yazılar