SNS/SQS Cross-Account Fan-Out: AWS'de Multi-Account Event Dağıtımı
Amazon SNS ve SQS ile güvenli cross-account event dağıtımı: IAM policy'leri, KMS şifreleme, AWS CDK kurulumu ve production'da karşılaşılan yaygın sorunlar.
Cross-account SNS/SQS fan-out, publisher hesabındaki tek bir SNS topic’in başka hesaplara ait SQS queue’lara event dağıtmasını sağlıyor; iki taraf da diğerine credential vermek zorunda kalmıyor. İşin büyük kısmı izin tarafında: bir topic policy, bir queue policy ve şifreleme devreye girdiğinde ikisiyle de uyumlu bir KMS key policy.
Tasarımı üç varsayılan taşıyor. Subscription’ı queue owner oluştursun, şifreli her queue customer-managed bir KMS key kullansın ve filtreleme SNS subscription seviyesinde yapılsın ki consumer’lar zaten atacakları mesajların ücretini ödemesin. CDK bağlantıları, FIFO sıralaması ve hesaplar arası dashboard’lar bu üç kararın üstüne biniyor.
Pattern Ne Kazandırıyor#
Multi-account bir AWS Organization’da bir ekibin ürettiği event’ler nadiren yalnızca o ekibi ilgilendiriyor. Hesaplar ekibe, servise veya environment’a göre ayrıldıktan sonra iş, ayrımı anlamlı kılan sınırı delmeden bu event’leri paylaşmaya geliyor.
Pattern’in yerini hak ettiği dört nokta var:
Yönetimsel izolasyon: Her hesap kendi kaynaklarını bağımsız olarak kontrol ediyor. Billing ekibi, ikisi de aynı kaynaktan event alsa bile fulfillment ekibinin altyapısını yanlışlıkla silemez.
Bağımsız scaling: Consumer hesaplar SQS processing’ini bağımsız olarak scale ediyor. Yavaş bir consumer yalnızca kendi hesabındaki kendi queue’sunu şişiriyor, diğerleri işlemeye devam ediyor.
Maliyet verimliliği: SNS’ten SQS’e delivery ücretsiz; yalnızca SNS publish’leri ve SQS operasyonları için ödeme yapıyorsun. Delivery ayağının kendisi için hiçbir ücret çıkmıyor, HTTP/S subscriber’larda ise her delivery ayrıca faturalanıyor.
Güvenlik sınırları: Her hesap kendi şifreleme, access policy’leri ve compliance kontrollerini implement ediyor. Security ekibi, publisher’da hiçbir değişiklik istemeden kendi hesabında sıkı key management uygulayabiliyor.
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. Aşağıdaki pattern’ler production’da güvenilir şekilde çalışır.
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 yalnızca subscription oluşturmayı kapsıyor; topic’e publish etmek ayrı bir izin
- 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:SourceArnileCondition, başka SNS topic’lerin bu queue’ya göndermesini engelliyorrawMessageDelivery: falsemesajı SNS metadata’sıyla sarıyor (debugging için öneriliyor)- Sadece mesaj body’sini istiyorsan, SNS envelope’ını atlamak için
rawMessageDelivery: trueyap - Long polling (
receiveMessageWaitTime) boş receive’leri ve maliyeti azaltıyor - Subscription’ın dead-letter queue’su, subscription ile aynı hesapta ve aynı region’da olmak zorunda; bu yüzden her consumer hesap kendi DLQ’sunu tutuyor
Çift Taraflı İzin#
Cross-account subscription için iki hesabın da izin vermesi gerekiyor:
- 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 çift taraflı izin zorunlu. İki taraftan birinde policy eksikse aynı “Access Denied” hatasını alıyorsun, o yüzden ölü bir subscription’ı ararken hatayı bildiren hesaba değil, iki hesaba birden bakmak gerekiyor.
KMS Şifreleme Konfigürasyonu#
Şifreleme cross-account setup’lara karmaşıklık ekliyor. AWS-managed key’ler hesap sınırları arasında çalışmadığı için şifreli bir queue’nun customer-managed key’e ihtiyacı oluyor.
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
Maliyeti#
Customer-managed KMS key’ler key başına ayda $1’a, artı 10.000 istek başına $0.03’e mal oluyor. Şifreli cross-account delivery’de sığınılacak ücretsiz bir seçenek yok; consumer hesap başına bir key’i bütçeye ekle.
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#
Payload-based filtreleme doğrudan mesaj body’si üzerinde eşleşiyor; publisher’ın routing alanlarını bir de message attribute olarak yazmasına gerek kalmıyor:
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ı:
- Her subscriber yalnızca policy’sine uyan mesajları alıyor ve SQS istek ücretini de yalnızca onlar için ödüyor
- Kazanç, subscriber’ın topic hacminin ne kadarını elediğine bağlı; yoğun bir topic’teki dar kapsamlı consumer en çok kazanan taraf oluyor
- Filter değişikliklerinin yayılması 15 dakikayı bulabiliyor
- Kota topic başına 200, hesap başına 10.000 filter policy
FIFO Topic’ler ve Queue’lar#
FIFO (First-In-First-Out) topic’ler kesin sıralama ve deduplication 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,
// Throughput mesaj grubu başına sayılıyor, grup başına saniyede 300 mesaj
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
})
);
Throughput kapsamında dikkat edilecekler:
- Varsayılan olan
FifoThroughputScope: Topic, FIFO topic’i saniyede 3.000 mesaj veya 20 MB ile sınırlıyor; hangisi önce dolarsa FifoThroughputScope: MessageGroupthroughput’u grup başına sayıyor ve grup başına tavan saniyede 300 mesaj; yani topic’in toplam throughput’u aktif grup sayısıyla birlikte artıyor- Hesap bazındaki publish kotası bunun üstünde ayrıca işliyor ve region’a göre değişiyor: us-east-1’de saniyede 30.000 mesaj, çoğu region’da 3.000
- Mesaj gruplarını hâlihazırda doğal paralelliği olan bir alana göre böl (müşteri, region, tenant); tek bir global grup ID’si tüm topic’i sıraya sokuyor
Yaygın Hatalar ve Çözümler#
Production’da tutarlı biçimde ortaya çıkan sorunlar ve çözümleri aşağıda.
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)
);
// Subscribe eden queue owner olduğu için onay otomatik
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']
)
URL üzerinden onaylamanın bilinmesi gereken bir yan etkisi var. AuthenticateOnUnsubscribe yalnızca subscription’ı queue owner oluşturduğunda otomatik olarak açılıyor ve kimlik doğrulaması olmadan açılmış bir link için sonradan açılamıyor. Bu durumda kimliği doğrulanmamış bir unsubscribe isteği subscription’ı düşürebiliyor.
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: Her iki policy de doğru görünmesine rağmen mesajlar hiç ulaşmıyor veya subscription reddediliyor.
Kök neden: Topic ile queue farklı region’larda, ama zincirdeki bir parça hâlâ tek bir region’a sabitlenmiş durumda. En sık iki suçlu var: topic’in region’ını içinde taşıyan aws:SourceArn condition’ı ve subscription ile aynı hesapta, aynı region’da olmak zorunda olan dead-letter queue.
Çözüm: Topic’i ve queue’ları tek region’da tutmak daha basit varsayılan; üstelik region’lar arası delivery gecikmesini ve veri transferi ücretini de ortadan kaldırıyor. Bir consumer gerçekten başka region’da çalışmak zorundaysa, hedef region’daki bir topic’e yeniden publish eden Lambda üzerinden forward et:
// 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, free tier hariç):
- SNS yayınları: 1M × $0.50 = $0.50
- SNS’den SQS’e delivery: ÜCRETSİZ
- SQS alımları: 4M × $0.40 = $1.60
- SQS silmeleri: 4M × $0.40 = $1.60
- Toplam: $3.70
%50 mesaj filtreleme ile:
- SNS yayınları: 1M × $0.50 = $0.50
- Filtreli delivery’ler: 2M mesaj gönderildi
- SQS alımları: 2M × $0.40 = $0.80
- SQS silmeleri: 2M × $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
- İstek hacmi
KmsDataKeyReusePeriodSecondsdeğerine bağlı: uzun bir yeniden kullanım penceresi tek bir data key’i çok sayıda mesaja yayıyor
Maliyet Optimizasyon Stratejileri#
- Subscription seviyesinde filtrele: consumer’lar zaten atacakları mesajlar için receive ve delete ücreti ödemiyor
- SQS long polling’i aktif et: tek bir ücretli receive, boş dönmek yerine 20 saniyeye kadar bekliyor
- Batch operasyonlarını kullan: API çağrısı başına 10 mesaja kadar
- Mesajları 64 KB altında tut: her 64 KB’lık parça ayrı bir istek olarak faturalanıyor
- Sıralama kritik değilse Standard queue kullan: milyon istek başına $0.50 yerine $0.40
Alternatif Yaklaşımlar#
Fan-out’un yetmediği yerde üç komşu servis devreye giriyor: routing karmaşıklığı, replay ihtiyacı ve senkron 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
Ödünleşimler:
- Milyon event başına $1.00; aynı hacim SNS’te $0.50
- JSONPath benzeri syntax ile daha güçlü filtreleme
- Yerleşik schema discovery ve validation
- Native cross-account event bus’ları
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
const bus = new events.EventBus(this, 'CentralBus', {
eventBusName: 'central-events',
});
// Cross-account erişim izni ver
bus.grantPutEventsTo(new iam.AccountPrincipal('111111111111'));
// Karmaşık filtreleme
new events.Rule(this, 'HighValueOrders', {
eventBus: bus,
eventPattern: {
source: ['com.myapp.orders'],
detailType: ['OrderCreated'],
detail: {
amount: [{ numeric: ['>', 1000] }],
region: ['us-east-1', 'us-west-2'],
status: ['pending', 'processing'],
},
},
targets: [new targets.SqsQueue(queue)],
});
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ı
Ödünleşimler:
- 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ığı
Ödünleşimler:
- Yerleşik retry queue’ları yok
- Cold start etkisi hesaba katılmalı
- Lambda concurrency ile sınırlı
- Scaling için queue’lardan daha az esnek
Uçtan Uca Multi-Account Kurulumu#
Publisher ve consumer stack’leri, birbirine bağlanmış hâliyle:
// 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,
});
}
}
Varsayılanlar Nerede Geçerli#
Queue owner’ın açtığı subscription, customer-managed key ve subscription seviyesinde filtreleme sıradan durumu karşılıyor: birkaç hesap, tek region ve S3 pointer’ı yerine mesajın kendisi olarak taşınabilecek kadar küçük event’ler. Birinci varsayılan, publisher kontrol etmediği hesaplar adına subscription açmak zorunda kaldığında bozuluyor; onay otomasyonu ve beraberindeki unsubscribe uyarısı tam da orada işe yarıyor. Bir compliance sınırı ikincisini daha ileri taşıyabilir: queue başına ayrı key ve daha dar bir kms:ViaService kapsamı. Doğrudan atlanacak olansa üçüncüsü; consumer gerçekten her mesajı istiyorsa filter policy hiçbir şey kazandırmıyor, üstüne hesaba katılacak bir yayılma gecikmesi ekliyor.
Bunlardan önce tasarımı yeniden şekillendiren iki kısıt var. Sıralama gereksinimi iki ucu birden FIFO’ya taşıyor ve throughput kolu mesaj grubu bölümlemesi hâline geliyor. İkinci bir region delivery’yi forwarder’ın arkasına alıyor, çünkü subscription’ın dead-letter queue’su topic’in peşinden region değiştiremiyor. Attribute ve payload eşleştirmesini aşan routing kuralları veya replay ihtiyacı ise EventBridge ya da Kinesis’i işaret ediyor.
Kaynaklar#
- SNS Mesajlarını Farklı Hesaptaki SQS Kuyruğuna Gönderme (yeni sekmede açılır) - Kaynak politikalarıyla çapraz hesap SNS-SQS aboneliklerini yapılandırmak için resmi adım adım kılavuz
- SNS Konusuna SQS Kuyruğu Abone Etme (yeni sekmede açılır) - Aynı hesap ve çapraz hesap kurulumu dahil fanout abonelik pattern’i için temel dokümantasyon
- Amazon SNS Mesaj Filtreleme (yeni sekmede açılır) - Abonelik filtre politikalarının hangi mesajın hangi aboneye ulaşacağını nasıl belirlediği; hem mesaj attribute’ları hem de mesaj body’si kapsamı için
- Amazon SNS Endpoint’leri ve Kotaları (yeni sekmede açılır) - Topic ve hesap başına filtre politikası limitleri, region’a göre FIFO publish throughput’u ve 256 KiB mesaj boyutu tavanı
- Amazon SNS Dead-Letter Kuyrukları (yeni sekmede açılır) - Redrive politikasının davranışı ve DLQ’nun abonelikle aynı hesapta, aynı region’da olma zorunluluğu
- Amazon SQS Dead-Letter Kuyrukları (yeni sekmede açılır) - Abone hesaplarda başarısız mesajları izole etmek için DLQ yapılandırması
- Amazon SQS Görünürlük Zaman Aşımı (yeni sekmede açılır) - Görünürlük zaman aşımının tüketici işleme ve Lambda entegrasyonuyla etkileşimi
- Amazon SNS Geliştirici Kılavuzu (yeni sekmede açılır) - Konular, abonelikler ve mesaj teslimatını kapsayan tam SNS servis dokümantasyonu
İlgili yazılar
SQS, SNS ve EventBridge arasında özelliklere göre değil iletişim modeline göre seçim yap; çalışan CDK örnekleri ve net bir maliyet analizi ile.
sqs · sns · eventbridge +5
İç servis katmanı kurmadan önce kurup kurmayacağınıza karar verin. Katmanın çağrı başına maliyeti, VPC Lattice'in kazandığı hacim ve direct invoke'un hâlâ kazandığı an.
aws · aws-cdk · lambda +4
Private REST API gRPC’yi yapısal olarak taşıyamaz ve gRPC konuşan her AWS yüzeyi Lambda hedeflerini dışlar. gRPC’den neyi tutmalı, neyi bırakmalı.
aws · aws-cdk · lambda +4
Private REST API, onu açan resource policy, route bazlı AWS_IAM yetkileri, iki CDK stack'i ve Node 22 Lambda'dan çağrıyı imzalamak.
aws · aws-cdk · lambda +4
SigV4 hangi servisin çağırdığını kanıtlar, kimin adına çağırdığını değil. Doğrulanmış özneyi nasıl taşırsınız ve taşıma katmanı gerçekte neyi şifreler.
aws · aws-cdk · lambda +4