2025-12-10
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:
- SNS topic policy: Cross-account
sns:Subscribeizni veriyor - SQS queue policy: SNS service principal’ının
sqs:SendMessageyapmasına izin veriyor - 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:
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çinArnPrincipalkullan sns:Subscribeaction’ı 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:
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:SourceArnileConditiondiğer SNS topic’lerin queue’na göndermesini engelliyorrawMessageDelivery: falsemesajı SNS metadata’sıyla sarıyor (debugging için öneriliyor)rawMessageDelivery: trueyap 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:
- Publisher subscription’a izin veriyor: SNS topic policy consumer hesaba
sns:Subscribeveriyor - Consumer mesajları kabul ediyor: SQS queue policy SNS service principal’ının mesaj göndermesine izin veriyor
- 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:
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ç duyuyorkms:GenerateDataKey: Envelope encryption için gereklikms:ViaServicecondition: 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 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:
// Publisher: Attribute'larla yayınla
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: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',
},
},
})
);
// Consumer: Filter policy ile subscribe ol
import * 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:
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
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 olabilir
fifoTopic.addSubscription(
new subscriptions.SqsSubscription(fifoQueue)
);
FIFO topic’lere yayınlama:
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:
// 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:
# Subscription'ları otomatik onaylamak için Python scripti
import boto3
import json
sqs = boto3.client('sqs')
queue_url = 'https://sqs.us-east-1.amazonaws.com/111111111111/service-events'
# Confirmation mesajı için poll et
response = 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:
// KMS key policy'nin bunu içerdiğinden emin ol
queueKey.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:
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:
// Region 1 (us-east-1): Orijinal topic
const sourceTopicEast = new sns.Topic(this, 'SourceTopicEast', {
topicName: 'events-us-east-1',
});
// Lambda forwarder region 2 topic'ine yayınlıyor
const 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:
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:
// Mesaj attribute'larının filter beklentileriyle eşleştiğini doğrula
await 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 izle
const 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
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ı izle
new 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 izle
new 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
// Queue depth'i izle
new 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 izle
new 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 izle
new 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:
import * as oam from 'aws-cdk-lib/aws-oam';
// Monitoring account'ta: Sink oluştur
const 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ştur
const 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
- SNS’den SQS’e delivery: ÜCRETSİZ
- SQS alımları: 4M × 1.60
- SQS silmeleri: 4M × 1.60
- Toplam: $3.70
%50 mesaj filtreleme ile:
- SNS yayınları: 1M × 0.50
- Filtreli delivery’ler: 2M mesaj gönderildi
- SQS alımları: 2M × 0.80
- SQS silmeleri: 2M × 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
- Mesaj filtreleme uygula: Tipik senaryolarda %50-70 maliyet azalması
- SQS long polling’i aktif et: Boş alımları %90 azaltıyor
- Batch operasyonlarını kullan: API çağrılarında 10 kata kadar azalma
- Mesajları 64 KB altında tut: Multi-request ücretlerinden kaçın
- 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 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:
// publisher-stack.ts
import * 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.ts
import * 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
Özelliklere göre değil, iletişim modeline göre seçim yap. SQS, SNS ve EventBridge arasında karar vermek için pratik bir rehber; çalışan CDK örnekleri ve maliyet analizi ile.
AWS Cognito ve Verified Permissions ile SaaS yetkilendirme mimarisi. Cedar politika dili, çok kiracılı desenler, JWT token akışı, maliyet analizi ve TypeScript örnekleriyle yaygın hatalar.
AWS Verified Permissions, SpiceDB, OpenFGA, Cerbos ve OPA dahil harici yetkilendirme platformlarının tarafsız değerlendirmesi. Mimari desenler, maliyet analizi ve mühendislik ekipleri için karar çerçevesi.
AI agent geliştirmek için TypeScript SDK'larının pratik karşılaştırması - Vercel AI SDK, OpenAI Agents SDK ve AWS Bedrock entegrasyonu. Kod örnekleri, karar frameworkleri ve production patternleri içeriyor.
Amazon Cognito'nun gelişmiş özellikleri üzerine kapsamlı teknik kılavuz: özel authentication akışları, federation pattern'leri, multi-tenancy mimarileri, migration stratejileri ve production-grade güvenlik implementasyonu.