Skip to content

Commit

Permalink
fix(sns-subscriptions): enable cross region subscriptions to sqs and …
Browse files Browse the repository at this point in the history
…lambda (#17273)

fixing cross region subscriptions to SQS and Lambda. This PR handles a
couple different scenarios. This only applies to SNS topics that are
instanceof sns.Topic, it does not apply to imported topics (sns.ITopic).
The current behavior for imported topics will remain the same.

1. SNS Topic and subscriber (sqs or lambda) are created in separate
   stacks with explicit environment.

In this case if the `region` is different between the two stacks then
the topic region will be provided in the subscription.

2. SNS Topic and subscriber (sqs or lambda) are created in separate
   stacks, and _both_ are env agnostic (no explicit region is provided)

In this case a region is not specified in the subscription resource,
which means it is assumed that they are both created in the same region.
The alternatives are to either throw an error telling the user to
specify a region, or to create a CfnOutput with the topic region. Since
it should be a rare occurrence for a user to be deploying a CDK app with
multiple env agnostic stacks that are meant for different environments,
I think the best option is to assume same region.

3. SNS Topic and subscriber (sqs or lambda) are created in separate
   stacks, and _one_ of them are env agnostic (no explicit region is provided)

In this case if the SNS stack has an explicit environment then we will
provide that in the subscription resource (assume that it is cross
region). If the SNS stack is env agnostic then we will do nothing
(assume they are created in the same region).

fixes #7044,#13707


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
corymhall committed Nov 11, 2021
1 parent 841cf99 commit 3cd8d48
Show file tree
Hide file tree
Showing 7 changed files with 983 additions and 3 deletions.
16 changes: 15 additions & 1 deletion packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts
@@ -1,7 +1,7 @@
import * as iam from '@aws-cdk/aws-iam';
import * as lambda from '@aws-cdk/aws-lambda';
import * as sns from '@aws-cdk/aws-sns';
import { Names, Stack } from '@aws-cdk/core';
import { Names, Stack, Token } from '@aws-cdk/core';
import { SubscriptionProps } from './subscription';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
Expand Down Expand Up @@ -36,6 +36,12 @@ export class LambdaSubscription implements sns.ITopicSubscription {
principal: new iam.ServicePrincipal('sns.amazonaws.com'),
});

// if the topic and function are created in different stacks
// then we need to make sure the topic is created first
if (topic instanceof sns.Topic && topic.stack !== this.fn.stack) {
this.fn.stack.addDependency(topic.stack);
}

return {
subscriberScope: this.fn,
subscriberId: topic.node.id,
Expand All @@ -50,6 +56,14 @@ export class LambdaSubscription implements sns.ITopicSubscription {
private regionFromArn(topic: sns.ITopic): string | undefined {
// no need to specify `region` for topics defined within the same stack.
if (topic instanceof sns.Topic) {
if (topic.stack !== this.fn.stack) {
// only if we know the region, will not work for
// env agnostic stacks
if (!Token.isUnresolved(topic.stack.region) &&
(topic.stack.region !== this.fn.stack.region)) {
return topic.stack.region;
}
}
return undefined;
}
return Stack.of(topic).parseArn(topic.topicArn).region;
Expand Down
16 changes: 15 additions & 1 deletion packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts
@@ -1,7 +1,7 @@
import * as iam from '@aws-cdk/aws-iam';
import * as sns from '@aws-cdk/aws-sns';
import * as sqs from '@aws-cdk/aws-sqs';
import { Names, Stack } from '@aws-cdk/core';
import { Names, Stack, Token } from '@aws-cdk/core';
import { SubscriptionProps } from './subscription';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
Expand Down Expand Up @@ -61,6 +61,12 @@ export class SqsSubscription implements sns.ITopicSubscription {
}));
}

// if the topic and queue are created in different stacks
// then we need to make sure the topic is created first
if (topic instanceof sns.Topic && topic.stack !== this.queue.stack) {
this.queue.stack.addDependency(topic.stack);
}

return {
subscriberScope: this.queue,
subscriberId: Names.nodeUniqueId(topic.node),
Expand All @@ -76,6 +82,14 @@ export class SqsSubscription implements sns.ITopicSubscription {
private regionFromArn(topic: sns.ITopic): string | undefined {
// no need to specify `region` for topics defined within the same stack
if (topic instanceof sns.Topic) {
if (topic.stack !== this.queue.stack) {
// only if we know the region, will not work for
// env agnostic stacks
if (!Token.isUnresolved(topic.stack.region) &&
(topic.stack.region !== this.queue.stack.region)) {
return topic.stack.region;
}
}
return undefined;
}
return Stack.of(topic).parseArn(topic.topicArn).region;
Expand Down
@@ -0,0 +1,116 @@
[
{
"Resources": {
"MyTopic86869434": {
"Type": "AWS::SNS::Topic",
"Properties": {
"TopicName": "topicstackopicstackmytopicc43e67afb24f28bb94f9"
}
}
}
},
{
"Resources": {
"EchoServiceRoleBE28060B": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
]
]
}
]
}
},
"Echo11F3FB29": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"ZipFile": "exports.handler = function handler(event, _context, callback) {\n /* eslint-disable no-console */\n console.log('====================================================');\n console.log(JSON.stringify(event, undefined, 2));\n console.log('====================================================');\n return callback(undefined, event);\n}"
},
"Role": {
"Fn::GetAtt": [
"EchoServiceRoleBE28060B",
"Arn"
]
},
"Handler": "index.handler",
"Runtime": "nodejs10.x"
},
"DependsOn": [
"EchoServiceRoleBE28060B"
]
},
"EchoAllowInvokeTopicStackMyTopicC43E67AF32CF6EFA": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Fn::GetAtt": [
"Echo11F3FB29",
"Arn"
]
},
"Principal": "sns.amazonaws.com",
"SourceArn": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":sns:us-east-1:12345678:topicstackopicstackmytopicc43e67afb24f28bb94f9"
]
]
}
}
},
"EchoMyTopic4CB8819E": {
"Type": "AWS::SNS::Subscription",
"Properties": {
"Protocol": "lambda",
"TopicArn": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":sns:us-east-1:12345678:topicstackopicstackmytopicc43e67afb24f28bb94f9"
]
]
},
"Endpoint": {
"Fn::GetAtt": [
"Echo11F3FB29",
"Arn"
]
},
"Region": "us-east-1"
}
}
}
}
]
@@ -0,0 +1,35 @@
import * as lambda from '@aws-cdk/aws-lambda';
import * as sns from '@aws-cdk/aws-sns';
import * as cdk from '@aws-cdk/core';
import * as subs from '../lib';

/// !cdk-integ *
const app = new cdk.App();

const topicStack = new cdk.Stack(app, 'TopicStack', {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' },
});
const topic = new sns.Topic(topicStack, 'MyTopic', {
topicName: cdk.PhysicalName.GENERATE_IF_NEEDED,
});

const functionStack = new cdk.Stack(app, 'FunctionStack', {
env: { region: 'us-east-2' },
});
const fction = new lambda.Function(functionStack, 'Echo', {
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_10_X,
code: lambda.Code.fromInline(`exports.handler = ${handler.toString()}`),
});

topic.addSubscription(new subs.LambdaSubscription(fction));

app.synth();

function handler(event: any, _context: any, callback: any) {
/* eslint-disable no-console */
console.log('====================================================');
console.log(JSON.stringify(event, undefined, 2));
console.log('====================================================');
return callback(undefined, event);
}
@@ -0,0 +1,90 @@
[
{
"Resources": {
"MyTopic86869434": {
"Type": "AWS::SNS::Topic",
"Properties": {
"TopicName": "topicstackopicstackmytopicc43e67afb24f28bb94f9"
}
}
}
},
{
"Resources": {
"MyQueueE6CA6235": {
"Type": "AWS::SQS::Queue",
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
},
"MyQueuePolicy6BBEDDAC": {
"Type": "AWS::SQS::QueuePolicy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "sqs:SendMessage",
"Condition": {
"ArnEquals": {
"aws:SourceArn": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":sns:us-east-1:12345678:topicstackopicstackmytopicc43e67afb24f28bb94f9"
]
]
}
}
},
"Effect": "Allow",
"Principal": {
"Service": "sns.amazonaws.com"
},
"Resource": {
"Fn::GetAtt": [
"MyQueueE6CA6235",
"Arn"
]
}
}
],
"Version": "2012-10-17"
},
"Queues": [
{
"Ref": "MyQueueE6CA6235"
}
]
}
},
"MyQueueTopicStackMyTopicC43E67AFC8DC8B4A": {
"Type": "AWS::SNS::Subscription",
"Properties": {
"Protocol": "sqs",
"TopicArn": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":sns:us-east-1:12345678:topicstackopicstackmytopicc43e67afb24f28bb94f9"
]
]
},
"Endpoint": {
"Fn::GetAtt": [
"MyQueueE6CA6235",
"Arn"
]
},
"Region": "us-east-1"
}
}
}
}
]
@@ -0,0 +1,25 @@
import * as sns from '@aws-cdk/aws-sns';
import * as sqs from '@aws-cdk/aws-sqs';
import * as cdk from '@aws-cdk/core';
import * as subs from '../lib';

/// !cdk-integ *
const app = new cdk.App();

/// !show
const topicStack = new cdk.Stack(app, 'TopicStack', {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' },
});
const topic = new sns.Topic(topicStack, 'MyTopic', {
topicName: cdk.PhysicalName.GENERATE_IF_NEEDED,
});

const queueStack = new cdk.Stack(app, 'QueueStack', {
env: { region: 'us-east-2' },
});
const queue = new sqs.Queue(queueStack, 'MyQueue');

topic.addSubscription(new subs.SqsSubscription(queue));
/// !hide

app.synth();

0 comments on commit 3cd8d48

Please sign in to comment.