diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.js.snapshot/aws-cdk-sqs-event-target.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.js.snapshot/aws-cdk-sqs-event-target.template.json index 7bfa29e5df27d..5d1a01d5ad86b 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.js.snapshot/aws-cdk-sqs-event-target.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.js.snapshot/aws-cdk-sqs-event-target.template.json @@ -91,6 +91,13 @@ ] } }, + "Metadata": { + "guard": { + "SuppressedRules": [ + "SQS_NO_WORLD_ACCESSIBLE_INLINE" + ] + } + }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, @@ -135,6 +142,16 @@ }, "MyDeadLetterQueueD997968A": { "Type": "AWS::SQS::Queue", + "Properties": { + "SqsManagedSseEnabled": true + }, + "Metadata": { + "guard": { + "SuppressedRules": [ + "SQS_NO_WORLD_ACCESSIBLE_INLINE" + ] + } + }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, @@ -176,6 +193,84 @@ } ] } + }, + "StandardQueueD43B45BD": { + "Type": "AWS::SQS::Queue", + "Properties": { + "SqsManagedSseEnabled": true + }, + "Metadata": { + "guard": { + "SuppressedRules": [ + "SQS_NO_WORLD_ACCESSIBLE_INLINE" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "StandardQueuePolicy21A6CBEE": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl", + "sqs:SendMessage" + ], + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Fn::GetAtt": [ + "StandardQueueRule82F7813E", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "StandardQueueD43B45BD", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "StandardQueueD43B45BD" + } + ] + } + }, + "StandardQueueRule82F7813E": { + "Type": "AWS::Events::Rule", + "Properties": { + "ScheduleExpression": "rate(1 minute)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "StandardQueueD43B45BD", + "Arn" + ] + }, + "Id": "Target0", + "SqsParameters": { + "MessageGroupId": "MyMessageGroupId" + } + } + ] + } } }, "Parameters": { @@ -212,4 +307,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.js.snapshot/integ.json index 55138f5528af5..cbf8ec22e996b 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.js.snapshot/integ.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.js.snapshot/integ.json @@ -1,14 +1,21 @@ { "version": "21.0.0", "testCases": { - "integ.sqs-event-rule-target": { + "integ.sqs-event-rule-target/DefaultTest": { "stacks": [ "aws-cdk-sqs-event-target" ], "diffAssets": false, - "stackUpdateWorkflow": true + "stackUpdateWorkflow": true, + "allowDestroy": [ + "AWS::SQS::Queue", + "AWS::SQS::QueuePolicy", + "AWS::Events::Rule" + ], + "assertionStack": "integ.sqs-event-rule-target/DefaultTest/DeployAssert", + "assertionStackName": "integsqseventruletargetDefaultTestDeployAssert71C4FD7F" } }, "synthContext": {}, "enableLookups": false -} \ No newline at end of file +} diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.js.snapshot/manifest.json index ff23d69bce1c7..4682254b4f399 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.js.snapshot/manifest.json @@ -75,6 +75,24 @@ "data": "MyDeadLetterQueuePolicyCC35D52C" } ], + "/aws-cdk-sqs-event-target/StandardQueue/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StandardQueueD43B45BD" + } + ], + "/aws-cdk-sqs-event-target/StandardQueue/Policy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StandardQueuePolicy21A6CBEE" + } + ], + "/aws-cdk-sqs-event-target/StandardQueueRule/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "StandardQueueRule82F7813E" + } + ], "/aws-cdk-sqs-event-target/BootstrapVersion": [ { "type": "aws:cdk:logicalId", @@ -86,9 +104,27 @@ "type": "aws:cdk:logicalId", "data": "CheckBootstrapVersion" } + ], + "StandardQueueD43B45BD": [ + { + "type": "aws:cdk:logicalId", + "data": "StandardQueueD43B45BD" + } + ], + "StandardQueuePolicy21A6CBEE": [ + { + "type": "aws:cdk:logicalId", + "data": "StandardQueuePolicy21A6CBEE" + } + ], + "StandardQueueRule82F7813E": [ + { + "type": "aws:cdk:logicalId", + "data": "StandardQueueRule82F7813E" + } ] }, "displayName": "aws-cdk-sqs-event-target" } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.js.snapshot/tree.json index 756cd84861370..acd49777fda10 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.js.snapshot/tree.json @@ -295,6 +295,127 @@ "fqn": "@aws-cdk/aws-sqs.Queue", "version": "0.0.0" } + }, + "StandardQueue": { + "id": "StandardQueue", + "path": "aws-cdk-sqs-event-target/StandardQueue", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sqs-event-target/StandardQueue/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SQS::Queue", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sqs.CfnQueue", + "version": "0.0.0" + } + }, + "Policy": { + "id": "Policy", + "path": "aws-cdk-sqs-event-target/StandardQueue/Policy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sqs-event-target/StandardQueue/Policy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SQS::QueuePolicy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl", + "sqs:SendMessage" + ], + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Fn::GetAtt": [ + "StandardQueueRule82F7813E", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "StandardQueueD43B45BD", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "queues": [ + { + "Ref": "StandardQueueD43B45BD" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sqs.CfnQueuePolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sqs.QueuePolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-sqs.Queue", + "version": "0.0.0" + } + }, + "StandardQueueRule": { + "id": "StandardQueueRule", + "path": "aws-cdk-sqs-event-target/StandardQueueRule", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-sqs-event-target/StandardQueueRule/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Events::Rule", + "aws:cdk:cloudformation:props": { + "scheduleExpression": "rate(1 minute)", + "state": "ENABLED", + "targets": [ + { + "id": "Target0", + "arn": { + "Fn::GetAtt": [ + "StandardQueueD43B45BD", + "Arn" + ] + }, + "sqsParameters": { + "messageGroupId": "MyMessageGroupId" + } + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-events.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-events.Rule", + "version": "0.0.0" + } } }, "constructInfo": { @@ -308,4 +429,4 @@ "version": "0.0.0" } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.ts index 876e6afc4130a..ff31d4440c444 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sqs/integ.sqs-event-rule-target.ts @@ -3,6 +3,7 @@ import * as kms from 'aws-cdk-lib/aws-kms'; import * as sqs from 'aws-cdk-lib/aws-sqs'; import * as cdk from 'aws-cdk-lib'; import * as targets from 'aws-cdk-lib/aws-events-targets'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; // --------------------------------- // Define a rule that triggers an SNS topic every 1min. @@ -23,11 +24,45 @@ const queue = new sqs.Queue(stack, 'MyQueue', { encryption: sqs.QueueEncryption.KMS, encryptionMasterKey: key, }); +// Suppress false positive: queue uses separate QueuePolicy resource (not inline), which is the correct pattern +(queue.node.defaultChild as cdk.CfnResource).addMetadata('guard', { + SuppressedRules: ['SQS_NO_WORLD_ACCESSIBLE_INLINE'], +}); -const deadLetterQueue = new sqs.Queue(stack, 'MyDeadLetterQueue'); +const deadLetterQueue = new sqs.Queue(stack, 'MyDeadLetterQueue', { + encryption: sqs.QueueEncryption.SQS_MANAGED, +}); +// Suppress false positive: queue uses separate QueuePolicy resource (not inline), which is the correct pattern +(deadLetterQueue.node.defaultChild as cdk.CfnResource).addMetadata('guard', { + SuppressedRules: ['SQS_NO_WORLD_ACCESSIBLE_INLINE'], +}); event.addTarget(new targets.SqsQueue(queue, { deadLetterQueue, })); -app.synth(); +// Test messageGroupId support for standard (non-FIFO) queues +const standardQueue = new sqs.Queue(stack, 'StandardQueue', { + encryption: sqs.QueueEncryption.SQS_MANAGED, +}); +// Suppress false positive: queue uses separate QueuePolicy resource (not inline), which is the correct pattern +(standardQueue.node.defaultChild as cdk.CfnResource).addMetadata('guard', { + SuppressedRules: ['SQS_NO_WORLD_ACCESSIBLE_INLINE'], +}); + +const standardQueueEvent = new events.Rule(stack, 'StandardQueueRule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), +}); + +standardQueueEvent.addTarget(new targets.SqsQueue(standardQueue, { + messageGroupId: 'MyMessageGroupId', +})); + +new IntegTest(app, 'integ.sqs-event-rule-target', { + testCases: [stack], + allowDestroy: [ + 'AWS::SQS::Queue', + 'AWS::SQS::QueuePolicy', + 'AWS::Events::Rule', + ], +}); diff --git a/packages/aws-cdk-lib/aws-events-targets/README.md b/packages/aws-cdk-lib/aws-events-targets/README.md index 3cad3a29eff77..2ef94df133249 100644 --- a/packages/aws-cdk-lib/aws-events-targets/README.md +++ b/packages/aws-cdk-lib/aws-events-targets/README.md @@ -718,6 +718,52 @@ rule.addTarget(new targets.RedshiftQuery(workgroup.attrWorkgroupWorkgroupArn, { })); ``` +## Send events to an SQS queue + +Use the `SqsQueue` target to send events to an SQS queue. + +The code snippet below creates an event rule that sends events to an SQS queue every hour: + +```ts +const queue = new sqs.Queue(this, 'MyQueue'); + +const rule = new events.Rule(this, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.hours(1)), +}); + +rule.addTarget(new targets.SqsQueue(queue)); +``` + +### Using Message Group IDs + +You can specify a `messageGroupId` to ensure messages are processed in order. This parameter is required for FIFO queues and optional for standard queues: + +```ts +// FIFO queue - messageGroupId required +const fifoQueue = new sqs.Queue(this, 'MyFifoQueue', { + fifo: true, +}); + +const fifoRule = new events.Rule(this, 'FifoRule', { + schedule: events.Schedule.rate(cdk.Duration.hours(1)), +}); + +fifoRule.addTarget(new targets.SqsQueue(fifoQueue, { + messageGroupId: 'MyMessageGroupId', +})); + +// Standard queue - messageGroupId optional (SQS Fair queue feature) +const standardQueue = new sqs.Queue(this, 'MyStandardQueue'); + +const standardRule = new events.Rule(this, 'StandardRule', { + schedule: events.Schedule.rate(cdk.Duration.hours(1)), +}); + +standardRule.addTarget(new targets.SqsQueue(standardQueue, { + messageGroupId: 'MyMessageGroupId', // Optional for standard queues +})); +``` + ## Publish to an SNS Topic Use the `SnsTopic` target to publish to an SNS Topic. diff --git a/packages/aws-cdk-lib/aws-events-targets/lib/sqs.ts b/packages/aws-cdk-lib/aws-events-targets/lib/sqs.ts index 52fe035e79320..f5f01ef3d361d 100644 --- a/packages/aws-cdk-lib/aws-events-targets/lib/sqs.ts +++ b/packages/aws-cdk-lib/aws-events-targets/lib/sqs.ts @@ -2,7 +2,7 @@ import { addToDeadLetterQueueResourcePolicy, TargetBaseProps, bindBaseTargetConf import * as events from '../../aws-events'; import * as iam from '../../aws-iam'; import * as sqs from '../../aws-sqs'; -import { FeatureFlags, ValidationError } from '../../core'; +import { FeatureFlags } from '../../core'; import * as cxapi from '../../cx-api'; /** @@ -13,9 +13,10 @@ export interface SqsQueueProps extends TargetBaseProps { /** * Message Group ID for messages sent to this queue * - * Required for FIFO queues, leave empty for regular queues. + * Required for FIFO queues. For standard queues, this parameter is optional + * and can be used for SQS fair queue feature and deduplication. * - * @default - no message group ID (regular queue) + * @default - no message group ID */ readonly messageGroupId?: string; @@ -41,9 +42,7 @@ export interface SqsQueueProps extends TargetBaseProps { */ export class SqsQueue implements events.IRuleTarget { constructor(public readonly queue: sqs.IQueue, private readonly props: SqsQueueProps = {}) { - if (props.messageGroupId !== undefined && !queue.fifo) { - throw new ValidationError('messageGroupId cannot be specified for non-FIFO queues', queue); - } + // messageGroupId is now supported for both FIFO and standard SQS queues } /** diff --git a/packages/aws-cdk-lib/aws-events-targets/test/sqs/sqs.test.ts b/packages/aws-cdk-lib/aws-events-targets/test/sqs/sqs.test.ts index 3f78664ad5dea..dedb97d5f0283 100644 --- a/packages/aws-cdk-lib/aws-events-targets/test/sqs/sqs.test.ts +++ b/packages/aws-cdk-lib/aws-events-targets/test/sqs/sqs.test.ts @@ -258,12 +258,37 @@ test('Encrypted queues result in a permissive policy statement when the feature }); }); -test('fail if messageGroupId is specified on non-fifo queues', () => { +test('messageGroupId is supported for standard (non-FIFO) queues', () => { const stack = new Stack(); const queue = new sqs.Queue(stack, 'MyQueue'); + const rule = new events.Rule(stack, 'MyRule', { + schedule: events.Schedule.rate(Duration.hours(1)), + }); + + // WHEN + rule.addTarget(new targets.SqsQueue(queue, { + messageGroupId: 'MyMessageGroupId', + })); - expect(() => new targets.SqsQueue(queue, { messageGroupId: 'MyMessageGroupId' })) - .toThrow(/messageGroupId cannot be specified/); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { + ScheduleExpression: 'rate(1 hour)', + State: 'ENABLED', + Targets: [ + { + Arn: { + 'Fn::GetAtt': [ + 'MyQueueE6CA6235', + 'Arn', + ], + }, + Id: 'Target0', + SqsParameters: { + MessageGroupId: 'MyMessageGroupId', + }, + }, + ], + }); }); test('fifo queues are synthesized correctly', () => {