feat(sns-subscriptions): support cross-region delivery from opt-in regions - #37890
feat(sns-subscriptions): support cross-region delivery from opt-in regions#37890DavidA94 wants to merge 2 commits into
Conversation
…gions When an SNS topic and its Lambda or SQS subscriber straddle an opt-in region (e.g. `ap-east-1`, `me-south-1`), AWS requires the subscriber's resource policy to trust the regionalized SNS service principal `sns.<region>.amazonaws.com` instead of (or in addition to) the default `sns.amazonaws.com`. `LambdaSubscription` and `SqsSubscription` previously hardcoded the default principal, so this scenario could not be expressed through the L2 API. Add two optional, backward-compatible properties to both subscription types: - `includeDefaultServicePrincipal` (default `true`) controls whether the default `sns.amazonaws.com` principal is granted permission. - `additionalServicePrincipalRegions` lists opt-in regions whose regionalized SNS service principals should also be granted permission. Both unset preserves today's behavior exactly. Synth-time validation against `aws-cdk-lib/region-info` rejects known default-enabled regions and tokenized values, and warns when a region is not yet recognized so customers are not blocked when AWS launches a new opt-in region. Lambda emits one `Lambda::Permission` per principal (the CFN resource type accepts only a single principal); the original logical ID is preserved for the default-principal permission so existing stacks are not affected. SQS adds all principals to a single resource-policy statement. The same regional principals are also threaded through to the dead-letter queue resource policy. A new optional `deadLetterQueueServicePrincipals` prop on `SubscriptionOptions` lets callers (including the L2 `Subscription` class directly) configure which principals can write to the DLQ; when omitted, today's default of `sns.amazonaws.com` is preserved. Integration test snapshots were generated with `--dry-run` because the realistic scenario (topic in an opt-in region) cannot be deployed without the AWS account having opted into that region. This follows the precedent set by `integ.dynamodb-v2.cross-account-replica.ts`, whose top-of-file comment explicitly instructs contributors to use `--dry-run` for non-deployable scenarios. fixes aws#37873 Co-authored-by: Kiro 🤖 <noreply@kiro.dev>
|
Closing in favor of #38572, which extends the auto-detecting While this PR was awaiting review, #38339 landed with a different design for the same underlying problem: instead of adding explicit opt-in props ( However, #38339 only touched Thanks to whoever eventually gets to review these — apologies for the wait on this one, and thanks to @xkjjx for driving the SQS fix in the meantime. |
|
Comments on closed issues and PRs are hard for our team to see. |
Issue # (if applicable)
Closes #37873.
Reason for this change
Amazon SNS supports cross-region delivery to AWS Lambda functions and Amazon SQS queues, but when one of the regions is an opt-in region (
ap-east-1,me-south-1,eu-south-1,af-south-1,il-central-1, etc.) the subscriber's resource policy must trust the regionalized SNS service principalsns.<region>.amazonaws.cominstead of (or in addition to) the defaultsns.amazonaws.com. AWS documents this rule on the cross-region delivery page and the Lambda prerequisites page.LambdaSubscriptionandSqsSubscriptionpreviously hardcodedsns.amazonaws.com, so customers could not express this scenario through the L2 API. The dead-letter queue resource policy created by the L2Subscriptionclass inaws-sns/lib/subscription.tshad the same gap. Workarounds (drop down to L1CfnSubscription, or hand-attach an extra resource-policy statement after the fact) are unreasonable for a documented and supported AWS scenario.Description of changes
Adds two optional, backward-compatible properties to both
LambdaSubscriptionPropsandSqsSubscriptionProps:includeDefaultServicePrincipal(defaulttrue) — controls whether the defaultsns.amazonaws.comprincipal is granted permission. Set tofalseonly when the topic is in an opt-in region and the subscriber should not also accept invocations from default-enabled regions.additionalServicePrincipalRegions— opt-in regions whose regionalized SNS service principals should also be granted permission.Both properties unset preserves today's behavior exactly. Existing CloudFormation logical IDs are preserved when the default principal is enabled, so existing stacks are not affected.
The shared logic (validation + principal construction) lives in a private helper
snsServicePrincipalsinaws-sns-subscriptions/lib/private/util.ts, consumed by both subscription types. Validation rules:aws-cdk-lib/region-info) are accepted.RegionInfoproduce a synth-time warning so customers are not blocked when AWS launches a new opt-in region beforeregion-infocatches up.Lambda emits one
AWS::Lambda::Permissionresource per principal, since that CFN resource type only accepts a single string in itsPrincipalfield. The original logical ID is preserved for the default-principal permission. SQS adds all principals to a single resource-policy statement (and to the KMS key policy when the queue is encrypted).The same regional principals are also threaded through to the dead-letter queue resource policy. A new optional
deadLetterQueueServicePrincipalsprop onSubscriptionOptions(inaws-sns/lib/subscription.ts) lets the L2Subscriptionclass — directly, or via the helpers — configure which principals can write to the DLQ. When omitted, today's default ofsns.amazonaws.comis preserved.Alternatives considered
'default'magic string (e.g.principalRegions: ['default', 'ap-east-1']). Rejected: magic strings are inconsistent with the rest ofaws-cdk-lib, and splitting into two props makes each prop's job unambiguous.SubscriptionPropsbase interface inaws-sns-subscriptions/lib/subscription.ts. Rejected: it would expose the props onEmailSubscription,SmsSubscription,UrlSubscription, andFirehoseSubscription, where they would be silently ignored — a footgun. Direct addition toLambdaSubscriptionPropsandSqsSubscriptionPropskeeps the type system honest./^[a-z]{2,}-[a-z]+-\d+$/as a pre-check. Rejected: the codebase has no precedent for regex-based region validation; the convention everywhere else (Stack.region,RegionInfo.regions,aws-lambda/lib/lambda-insights.ts) is to consultregion-info's known-region list. UsingFact.find(region, FactName.IS_OPT_IN_REGION)also produces a more accurate error message for cases likeus-gov-west-1("not an opt-in region" rather than the misleading "not a valid region identifier").iam.CompositePrincipalfor Lambda. Not viable — Lambda'saddPermissionrequires a principal that exposes a stringservice(oraccountId/arn) field, whichCompositePrincipaldoes not have.topic.env.regionandsubscriber.env.region. Rejected — would silently misbehave for env-agnostic stacks (wheretopic.env.regionis a Token) and for cross-account scenarios where the topic is imported by ARN. Explicit configuration matches CDK's convention of preferring user intent over inference for security-relevant policy.aws-snsvsaws-sns-subscriptions), but the user-facing feature is one cohesive thing — without the DLQ wiring, the same configuration that works for the main subscription would silently fail for its DLQ. Shipping the feature half-complete is worse than the slightly bigger diff.FirehoseSubscriptionis intentionally not changed: per the AWS cross-region delivery documentation, only Lambda and SQS support cross-region delivery. Firehose subscriptions use a customer-provided IAM role assumed via STS, so the regionalized service principal does not apply.Describe any new or updated permissions being added
No new IAM permissions are required to use the feature. The change adjusts the principal side of existing permissions:
AWS::Lambda::Permission— emits an additional resource per opt-in region withPrincipal: sns.<region>.amazonaws.com(only whenadditionalServicePrincipalRegionsis set).AWS::SQS::QueuePolicy(subscription queue) — addssns.<region>.amazonaws.comto theServicearray of the existing statement (same condition:aws:SourceArnequals the topic ARN).AWS::KMS::Keypolicy (when the SQS queue is encrypted) — addssns.<region>.amazonaws.comto theServicearray of thekms:Decrypt/kms:GenerateDataKeystatement.AWS::SQS::QueuePolicy(dead-letter queue) — addssns.<region>.amazonaws.comto theServicearray (sameaws:SourceArncondition).All additions are scoped by
aws:SourceArnto the specific topic and only fire when the user opts in viaadditionalServicePrincipalRegions. There is no broadening of the default permission set.Description of how you validated changes
Unit tests: added 14 new tests in
packages/aws-cdk-lib/aws-sns-subscriptions/test/subs.test.tscovering Lambda + SQS happy paths, theincludeDefaultServicePrincipal=falsepath, the DLQ path on both Lambda and SQS, the rejected-token path, the rejected default-enabled-region path, the rejected non-public-partition path, and the warning-on-unknown-region path. All 100 tests inaws-sns-subscriptions/test/andaws-sns/test/subscription.test.tspass, including the 86 pre-existing tests (so backwards compatibility is verified end-to-end).Integration tests: added two
IntegTest-based integ tests underpackages/@aws-cdk-testing/framework-integ/test/aws-sns-subscriptions/test/:integ.sns-lambda-opt-in-region.ts— topic inap-east-1, Lambda + DLQ inus-east-2. Exercises the end-to-end wiring including the DLQ resource policy.integ.sns-sqs-opt-in-region.lit.ts— topic inap-east-1, queue inus-east-2. The lit form is referenced from the README example.Snapshot generation note: the realistic scenario (topic in an opt-in region) cannot be deployed in CI without an AWS account that has opted into
ap-east-1. Snapshots were generated withyarn integ --dry-run --update-on-failed, which is generally discouraged byCONTRIBUTING.mdbut follows the established precedent ininteg.dynamodb-v2.cross-account-replica.ts, whose top-of-file comment explicitly instructs contributors to use--dry-runfor non-deployable scenarios. A maintainer with an opt-in-region account can re-validate against a real deployment if desired; the integ test files include top-of-file comments documenting how to do so.Build: clean
yarn buildforaws-cdk-liband@aws-cdk-testing/framework-integ.npx lerna run buildacross all 61 jsii packages succeeds.jsii-rosetta extract --compilesucceeds for all README snippets, including the two new examples.Checklist
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license