diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/README.md b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/README.md index 4dd8d355f..4f4c3187b 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/README.md @@ -65,6 +65,8 @@ _Parameters_ |existingBucketObj?|[`s3.IBucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.IBucket.html)|Existing instance of S3 Bucket object, providing both this and `bucketProps` will cause an error.| |bucketProps?|[`s3.BucketProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html)|User provided props to override the default props for the S3 Bucket. If this is provided, then also providing bucketProps is an error. | |logGroupProps?|[`logs.LogGroupProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-logs.LogGroupProps.html)|User provided props to override the default props for for the CloudWatchLogs LogGroup.| +|loggingBucketProps?|[`s3.BucketProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html)|Optional user provided props to override the default props for the S3 Logging Bucket.| +|logS3AccessLogs? | boolean|Whether to turn on Access Logging for the S3 bucket. Creates an S3 bucket with associated storage costs for the logs. Enabling Access Logging is a best practice. default - true| ## Pattern Properties @@ -77,6 +79,7 @@ _Parameters_ |iotActionsRole|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iam.Role.html)|Returns an instance of the iam.Role created by the construct for IoT Rule| |kinesisFirehoseRole|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iam.Role.html)|Returns an instance of the iam.Role created by the construct for Kinesis Data Firehose delivery stream| |kinesisFirehoseLogGroup|[`logs.LogGroup`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-logs.LogGroup.html)|Returns an instance of the LogGroup created by the construct for Kinesis Data Firehose delivery stream| +|s3BucketInterface|[`s3.IBucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.IBucket.html)|Returns an instance of s3.IBucket created by the construct| ## Default settings diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/lib/index.ts index dd8464216..4a2573510 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/lib/index.ts @@ -43,19 +43,32 @@ export interface IotToKinesisFirehoseToS3Props { * * @default - None */ - readonly existingBucketObj?: s3.IBucket, + readonly existingBucketObj?: s3.IBucket; /** * User provided props to override the default props for the S3 Bucket. * * @default - Default props are used */ - readonly bucketProps?: s3.BucketProps, + readonly bucketProps?: s3.BucketProps; /** * User provided props to override the default props for the CloudWatchLogs LogGroup. * * @default - Default props are used */ - readonly logGroupProps?: logs.LogGroupProps + readonly logGroupProps?: logs.LogGroupProps; + /** + * Optional user provided props to override the default props for the S3 Logging Bucket. + * + * @default - Default props are used + */ + readonly loggingBucketProps?: s3.BucketProps; + /** + * Whether to turn on Access Logs for the S3 bucket with the associated storage costs. + * Enabling Access Logging is a best practice. + * + * @default - true + */ + readonly logS3AccessLogs?: boolean; } export class IotToKinesisFirehoseToS3 extends Construct { @@ -66,6 +79,7 @@ export class IotToKinesisFirehoseToS3 extends Construct { public readonly s3Bucket?: s3.Bucket; public readonly s3LoggingBucket?: s3.Bucket; public readonly iotActionsRole: iam.Role; + public readonly s3BucketInterface: s3.IBucket; /** * @summary Constructs a new instance of the IotToKinesisFirehoseToS3 class. @@ -79,18 +93,17 @@ export class IotToKinesisFirehoseToS3 extends Construct { super(scope, id); defaults.CheckProps(props); - if (props.existingBucketObj && props.bucketProps) { - throw new Error('Cannot specify both bucket properties and an existing bucket'); - } - const firehoseToS3 = new KinesisFirehoseToS3(this, 'KinesisFirehoseToS3', { kinesisFirehoseProps: props.kinesisFirehoseProps, existingBucketObj: props.existingBucketObj, bucketProps: props.bucketProps, - logGroupProps: props.logGroupProps + logGroupProps: props.logGroupProps, + loggingBucketProps: props.loggingBucketProps, + logS3AccessLogs: props.logS3AccessLogs }); this.kinesisFirehose = firehoseToS3.kinesisFirehose; this.s3Bucket = firehoseToS3.s3Bucket; + this.s3BucketInterface = firehoseToS3.s3BucketInterface; // Setup the IAM Role for IoT Actions this.iotActionsRole = new iam.Role(this, 'IotActionsRole', { diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/test/integ.customLoggingBucket.expected.json b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/test/integ.customLoggingBucket.expected.json new file mode 100644 index 000000000..fd285b691 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/test/integ.customLoggingBucket.expected.json @@ -0,0 +1,468 @@ +{ + "Resources": { + "testiotkinesisfirehoses3KinesisFirehoseToS3S3LoggingBucket03F0BA8E": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "BucketName": "custom-logging-bucket", + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket" + } + ] + } + } + }, + "testiotkinesisfirehoses3KinesisFirehoseToS3S3LoggingBucketPolicyCCE58825": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3S3LoggingBucket03F0BA8E" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3KinesisFirehoseToS3S3LoggingBucket03F0BA8E", + "Arn" + ] + }, + "/*" + ] + ] + }, + { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3KinesisFirehoseToS3S3LoggingBucket03F0BA8E", + "Arn" + ] + } + ], + "Sid": "HttpsOnly" + } + ], + "Version": "2012-10-17" + } + } + }, + "testiotkinesisfirehoses3KinesisFirehoseToS3S3BucketAEE2D91B": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "LifecycleConfiguration": { + "Rules": [ + { + "NoncurrentVersionTransitions": [ + { + "StorageClass": "GLACIER", + "TransitionInDays": 90 + } + ], + "Status": "Enabled" + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3S3LoggingBucket03F0BA8E" + } + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "testiotkinesisfirehoses3KinesisFirehoseToS3S3BucketPolicy8BDDB6C2": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3S3BucketAEE2D91B" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3KinesisFirehoseToS3S3BucketAEE2D91B", + "Arn" + ] + }, + "/*" + ] + ] + }, + { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3KinesisFirehoseToS3S3BucketAEE2D91B", + "Arn" + ] + } + ], + "Sid": "HttpsOnly" + } + ], + "Version": "2012-10-17" + } + } + }, + "testiotkinesisfirehoses3KinesisFirehoseToS3firehoseloggroupDD0940D4": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely" + }, + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } + }, + "testiotkinesisfirehoses3KinesisFirehoseToS3firehoseloggroupfirehoselogstream93DF029F": { + "Type": "AWS::Logs::LogStream", + "Properties": { + "LogGroupName": { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3firehoseloggroupDD0940D4" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testiotkinesisfirehoses3KinesisFirehoseToS3KinesisFirehoseRole1BC69C9C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "firehose.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testiotkinesisfirehoses3KinesisFirehoseToS3KinesisFirehosePolicyABB5FC58": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:AbortMultipartUpload", + "s3:GetBucketLocation", + "s3:GetObject", + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + "s3:PutObject" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3KinesisFirehoseToS3S3BucketAEE2D91B", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3KinesisFirehoseToS3S3BucketAEE2D91B", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "logs:PutLogEvents", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:", + { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3firehoseloggroupDD0940D4" + }, + ":log-stream:", + { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3firehoseloggroupfirehoselogstream93DF029F" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testiotkinesisfirehoses3KinesisFirehoseToS3KinesisFirehosePolicyABB5FC58", + "Roles": [ + { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3KinesisFirehoseRole1BC69C9C" + } + ] + } + }, + "testiotkinesisfirehoses3KinesisFirehoseToS3KinesisFirehoseBD88A36B": { + "Type": "AWS::KinesisFirehose::DeliveryStream", + "Properties": { + "DeliveryStreamEncryptionConfigurationInput": { + "KeyType": "AWS_OWNED_CMK" + }, + "ExtendedS3DestinationConfiguration": { + "BucketARN": { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3KinesisFirehoseToS3S3BucketAEE2D91B", + "Arn" + ] + }, + "BufferingHints": { + "IntervalInSeconds": 300, + "SizeInMBs": 5 + }, + "CloudWatchLoggingOptions": { + "Enabled": true, + "LogGroupName": { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3firehoseloggroupDD0940D4" + }, + "LogStreamName": { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3firehoseloggroupfirehoselogstream93DF029F" + } + }, + "CompressionFormat": "GZIP", + "EncryptionConfiguration": { + "KMSEncryptionConfig": { + "AWSKMSKeyARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":kms:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":alias/aws/s3" + ] + ] + } + } + }, + "RoleARN": { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3KinesisFirehoseToS3KinesisFirehoseRole1BC69C9C", + "Arn" + ] + } + } + } + }, + "testiotkinesisfirehoses3IotActionsRole47344B2F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testiotkinesisfirehoses3IotActionsPolicy89971F7F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "firehose:PutRecord", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3KinesisFirehoseToS3KinesisFirehoseBD88A36B", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testiotkinesisfirehoses3IotActionsPolicy89971F7F", + "Roles": [ + { + "Ref": "testiotkinesisfirehoses3IotActionsRole47344B2F" + } + ] + } + }, + "testiotkinesisfirehoses3IotTopic4CCBBBDC": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Firehose": { + "DeliveryStreamName": { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3KinesisFirehoseBD88A36B" + }, + "RoleArn": { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3IotActionsRole47344B2F", + "Arn" + ] + } + } + } + ], + "Description": "Persistent storage of connected vehicle telematics data", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'connectedcar/telemetry/#'" + } + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store." + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/test/integ.customLoggingBucket.ts b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/test/integ.customLoggingBucket.ts new file mode 100644 index 000000000..9ba1a37c9 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/test/integ.customLoggingBucket.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack, RemovalPolicy } from "@aws-cdk/core"; +import { BucketEncryption } from "@aws-cdk/aws-s3"; +import { IotToKinesisFirehoseToS3 } from "../lib"; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; + +const app = new App(); + +// Empty arguments +const stack = new Stack(app, generateIntegStackName(__filename)); + +new IotToKinesisFirehoseToS3(stack, 'test-iot-kinesisfirehose-s3', { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Persistent storage of connected vehicle telematics data", + sql: "SELECT * FROM 'connectedcar/telemetry/#'", + actions: [] + } + }, + bucketProps: { + removalPolicy: RemovalPolicy.DESTROY, + }, + loggingBucketProps: { + removalPolicy: RemovalPolicy.DESTROY, + bucketName: 'custom-logging-bucket', + encryption: BucketEncryption.S3_MANAGED, + versioned: true + } +}); +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/test/integ.noLoggingBucket.expected.json b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/test/integ.noLoggingBucket.expected.json new file mode 100644 index 000000000..85c1b5272 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/test/integ.noLoggingBucket.expected.json @@ -0,0 +1,388 @@ +{ + "Resources": { + "testiotkinesisfirehoses3KinesisFirehoseToS3S3BucketAEE2D91B": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "LifecycleConfiguration": { + "Rules": [ + { + "NoncurrentVersionTransitions": [ + { + "StorageClass": "GLACIER", + "TransitionInDays": 90 + } + ], + "Status": "Enabled" + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is created for unit/ integration testing purposes only." + } + ] + } + } + }, + "testiotkinesisfirehoses3KinesisFirehoseToS3S3BucketPolicy8BDDB6C2": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3S3BucketAEE2D91B" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3KinesisFirehoseToS3S3BucketAEE2D91B", + "Arn" + ] + }, + "/*" + ] + ] + }, + { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3KinesisFirehoseToS3S3BucketAEE2D91B", + "Arn" + ] + } + ], + "Sid": "HttpsOnly" + } + ], + "Version": "2012-10-17" + } + } + }, + "testiotkinesisfirehoses3KinesisFirehoseToS3firehoseloggroupDD0940D4": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely" + }, + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } + }, + "testiotkinesisfirehoses3KinesisFirehoseToS3firehoseloggroupfirehoselogstream93DF029F": { + "Type": "AWS::Logs::LogStream", + "Properties": { + "LogGroupName": { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3firehoseloggroupDD0940D4" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testiotkinesisfirehoses3KinesisFirehoseToS3KinesisFirehoseRole1BC69C9C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "firehose.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testiotkinesisfirehoses3KinesisFirehoseToS3KinesisFirehosePolicyABB5FC58": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:AbortMultipartUpload", + "s3:GetBucketLocation", + "s3:GetObject", + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + "s3:PutObject" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3KinesisFirehoseToS3S3BucketAEE2D91B", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3KinesisFirehoseToS3S3BucketAEE2D91B", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "logs:PutLogEvents", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:", + { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3firehoseloggroupDD0940D4" + }, + ":log-stream:", + { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3firehoseloggroupfirehoselogstream93DF029F" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testiotkinesisfirehoses3KinesisFirehoseToS3KinesisFirehosePolicyABB5FC58", + "Roles": [ + { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3KinesisFirehoseRole1BC69C9C" + } + ] + } + }, + "testiotkinesisfirehoses3KinesisFirehoseToS3KinesisFirehoseBD88A36B": { + "Type": "AWS::KinesisFirehose::DeliveryStream", + "Properties": { + "DeliveryStreamEncryptionConfigurationInput": { + "KeyType": "AWS_OWNED_CMK" + }, + "ExtendedS3DestinationConfiguration": { + "BucketARN": { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3KinesisFirehoseToS3S3BucketAEE2D91B", + "Arn" + ] + }, + "BufferingHints": { + "IntervalInSeconds": 300, + "SizeInMBs": 5 + }, + "CloudWatchLoggingOptions": { + "Enabled": true, + "LogGroupName": { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3firehoseloggroupDD0940D4" + }, + "LogStreamName": { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3firehoseloggroupfirehoselogstream93DF029F" + } + }, + "CompressionFormat": "GZIP", + "EncryptionConfiguration": { + "KMSEncryptionConfig": { + "AWSKMSKeyARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":kms:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":alias/aws/s3" + ] + ] + } + } + }, + "RoleARN": { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3KinesisFirehoseToS3KinesisFirehoseRole1BC69C9C", + "Arn" + ] + } + } + } + }, + "testiotkinesisfirehoses3IotActionsRole47344B2F": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testiotkinesisfirehoses3IotActionsPolicy89971F7F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "firehose:PutRecord", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3KinesisFirehoseToS3KinesisFirehoseBD88A36B", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testiotkinesisfirehoses3IotActionsPolicy89971F7F", + "Roles": [ + { + "Ref": "testiotkinesisfirehoses3IotActionsRole47344B2F" + } + ] + } + }, + "testiotkinesisfirehoses3IotTopic4CCBBBDC": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Firehose": { + "DeliveryStreamName": { + "Ref": "testiotkinesisfirehoses3KinesisFirehoseToS3KinesisFirehoseBD88A36B" + }, + "RoleArn": { + "Fn::GetAtt": [ + "testiotkinesisfirehoses3IotActionsRole47344B2F", + "Arn" + ] + } + } + } + ], + "Description": "Persistent storage of connected vehicle telematics data", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'connectedcar/telemetry/#'" + } + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store." + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/test/integ.noLoggingBucket.ts b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/test/integ.noLoggingBucket.ts new file mode 100644 index 000000000..15c2e384e --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/test/integ.noLoggingBucket.ts @@ -0,0 +1,50 @@ +/** + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack, RemovalPolicy } from "@aws-cdk/core"; +import { IotToKinesisFirehoseToS3 } from "../lib"; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; +import * as s3 from "@aws-cdk/aws-s3"; +import * as defaults from '@aws-solutions-constructs/core'; + +const app = new App(); + +// Empty arguments +const stack = new Stack(app, generateIntegStackName(__filename)); + +const construct = new IotToKinesisFirehoseToS3(stack, 'test-iot-kinesisfirehose-s3', { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Persistent storage of connected vehicle telematics data", + sql: "SELECT * FROM 'connectedcar/telemetry/#'", + actions: [] + } + }, + bucketProps: { + removalPolicy: RemovalPolicy.DESTROY, + }, + logS3AccessLogs: false +}); + +const s3Bucket = construct.s3Bucket as s3.Bucket; + +defaults.addCfnSuppressRules(s3Bucket, [ + { + id: 'W35', + reason: 'This S3 bucket is created for unit/ integration testing purposes only.' + }, +]); + +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/test/test.iot-kinesisfirehose-s3.test.ts b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/test/test.iot-kinesisfirehose-s3.test.ts index e46c26994..c35d6fb39 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/test/test.iot-kinesisfirehose-s3.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/test/test.iot-kinesisfirehose-s3.test.ts @@ -152,5 +152,98 @@ test("Test bad call with existingBucket and bucketProps", () => { }); }; // Assertion - expect(app).toThrowError(); + expect(app).toThrowError('Error - Either provide bucketProps or existingBucketObj, but not both.\n'); +}); + +// -------------------------------------------------------------- +// s3 bucket with bucket, loggingBucket, and auto delete objects +// -------------------------------------------------------------- +test('s3 bucket with bucket, loggingBucket, and auto delete objects', () => { + const stack = new cdk.Stack(); + + new IotToKinesisFirehoseToS3(stack, 'iot-kinesisfirehose-s3', { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Persistent storage of connected vehicle telematics data", + sql: "SELECT * FROM 'connectedcar/telemetry/#'", + actions: [] + } + }, + bucketProps: { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }, + loggingBucketProps: { + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true + } + }); + + expect(stack).toHaveResource("AWS::S3::Bucket", { + AccessControl: "LogDeliveryWrite" + }); + + expect(stack).toHaveResource("Custom::S3AutoDeleteObjects", { + ServiceToken: { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", + "Arn" + ] + }, + BucketName: { + Ref: "iotkinesisfirehoses3KinesisFirehoseToS3S3LoggingBucketE63ABD3D" + } + }); +}); + +// -------------------------------------------------------------- +// Test bad call with logS3AccessLogs as false and bucketProps +// -------------------------------------------------------------- +test("Test bad call with logS3AccessLogs as false and bucketProps", () => { + // Stack + const stack = new cdk.Stack(); + + const app = () => { + // Helper declaration + new IotToKinesisFirehoseToS3(stack, "bad-s3-args", { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Persistent storage of connected vehicle telematics data", + sql: "SELECT * FROM 'connectedcar/telemetry/#'", + actions: [] + } + }, + loggingBucketProps: { + removalPolicy: cdk.RemovalPolicy.DESTROY + }, + logS3AccessLogs: false + }); + }; + // Assertion + expect(app).toThrowError('Error - If logS3AccessLogs is false, supplying loggingBucketProps or existingLoggingBucketObj is invalid.\n'); +}); + +// -------------------------------------------------------------- +// s3 bucket with one content bucket and no logging bucket +// -------------------------------------------------------------- +test('s3 bucket with one content bucket and no logging bucket', () => { + const stack = new cdk.Stack(); + + new IotToKinesisFirehoseToS3(stack, 'iot-kinsisfirehose-s3', { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Persistent storage of connected vehicle telematics data", + sql: "SELECT * FROM 'connectedcar/telemetry/#'", + actions: [] + } + }, + bucketProps: { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }, + logS3AccessLogs: false + }); + + expect(stack).toCountResources("AWS::S3::Bucket", 1); }); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/README.md b/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/README.md index 387a50218..358822016 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/README.md @@ -62,6 +62,7 @@ _Parameters_ |kinesisFirehoseRole|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iam.Role.html)|Returns an instance of the iam.Role created by the construct for Kinesis Data Firehose delivery stream| |s3Bucket?|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Returns an instance of s3.Bucket created by the construct| |s3LoggingBucket?|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Returns an instance of s3.Bucket created by the construct as the logging bucket for the primary bucket| +|s3BucketInterface|[`s3.IBucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.IBucket.html)|Returns an instance of s3.IBucket created by the construct| ## Default settings diff --git a/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts b/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts index 007a55d60..03d9598d3 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts @@ -494,4 +494,4 @@ test('Test fail false logAlbAccessLogs and albLoggingBucketProps check', () => { // Assertion expect(app).toThrowError('Error - If logAlbAccessLogs is false, supplying albLoggingBucketProps is invalid.\n'); -}); \ No newline at end of file +});