Description: "(uksb-1tcfnc0g5)" Resources: seseventsdestinationEA24EF5F: Type: AWS::S3::Bucket Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: aws:kms BucketName: Fn::Join: - "" - - Ref: AWS::AccountId - "-" - Ref: AWS::Region - -ses-events-destination OwnershipControls: Rules: - ObjectOwnership: BucketOwnerEnforced PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true UpdateReplacePolicy: Retain DeletionPolicy: Retain seseventsdestinationPolicyAC1B01B4: Type: AWS::S3::BucketPolicy Properties: Bucket: Ref: seseventsdestinationEA24EF5F PolicyDocument: Statement: - Action: s3:* Condition: Bool: aws:SecureTransport: "false" Effect: Deny Principal: AWS: "*" Resource: - Fn::GetAtt: - seseventsdestinationEA24EF5F - Arn - Fn::Join: - "" - - Fn::GetAtt: - seseventsdestinationEA24EF5F - Arn - /* Version: "2012-10-17" seseventsdestinationNotificationsDC46E6DF: Type: Custom::S3BucketNotifications Properties: ServiceToken: Fn::GetAtt: - BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691 - Arn BucketName: Ref: seseventsdestinationEA24EF5F NotificationConfiguration: LambdaFunctionConfigurations: - Events: - s3:ObjectCreated:* Filter: Key: FilterRules: - Name: prefix Value: partitioned/ LambdaFunctionArn: Fn::GetAtt: - SESPartitionedObjectReplicationFunction413D8164 - Arn Managed: true DependsOn: - seseventsdestinationAllowBucketNotificationsToSesBlogSolutionStackSESPartitionedObjectReplicationFunctionFC59373D512A5B8B seseventsdestinationAllowBucketNotificationsToSesBlogSolutionStackSESPartitionedObjectReplicationFunctionFC59373D512A5B8B: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: Fn::GetAtt: - SESPartitionedObjectReplicationFunction413D8164 - Arn Principal: s3.amazonaws.com SourceAccount: Ref: AWS::AccountId SourceArn: Fn::GetAtt: - seseventsdestinationEA24EF5F - Arn seseventsdestinationaggregatedD1CA1006: Type: AWS::S3::Bucket Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: aws:kms BucketName: Fn::Join: - "" - - Ref: AWS::AccountId - "-" - Ref: AWS::Region - -ses-events-destination-aggregated OwnershipControls: Rules: - ObjectOwnership: BucketOwnerEnforced PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true UpdateReplacePolicy: Retain DeletionPolicy: Retain seseventsdestinationaggregatedPolicy42D9EBC4: Type: AWS::S3::BucketPolicy Properties: Bucket: Ref: seseventsdestinationaggregatedD1CA1006 PolicyDocument: Statement: - Action: s3:* Condition: Bool: aws:SecureTransport: "false" Effect: Deny Principal: AWS: "*" Resource: - Fn::GetAtt: - seseventsdestinationaggregatedD1CA1006 - Arn - Fn::Join: - "" - - Fn::GetAtt: - seseventsdestinationaggregatedD1CA1006 - Arn - /* Version: "2012-10-17" SESPartitionedObjectReplicationFunctionServiceRole7A64D76B: 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 SESPartitionedObjectReplicationFunctionServiceRoleDefaultPolicy265F3C9E: Type: AWS::IAM::Policy Properties: PolicyDocument: Statement: - Action: - s3:GetBucket* - s3:GetObject* - s3:List* Effect: Allow Resource: - Fn::GetAtt: - seseventsdestinationEA24EF5F - Arn - Fn::Join: - "" - - Fn::GetAtt: - seseventsdestinationEA24EF5F - Arn - /* - Action: - s3:Abort* - s3:DeleteObject* - s3:PutObject - s3:PutObjectLegalHold - s3:PutObjectRetention - s3:PutObjectTagging - s3:PutObjectVersionTagging Effect: Allow Resource: - Fn::GetAtt: - seseventsdestinationaggregatedD1CA1006 - Arn - Fn::Join: - "" - - Fn::GetAtt: - seseventsdestinationaggregatedD1CA1006 - Arn - /* Version: "2012-10-17" PolicyName: SESPartitionedObjectReplicationFunctionServiceRoleDefaultPolicy265F3C9E Roles: - Ref: SESPartitionedObjectReplicationFunctionServiceRole7A64D76B SESPartitionedObjectReplicationFunction413D8164: Type: AWS::Lambda::Function Properties: Code: ZipFile: |- const { S3Client, CopyObjectCommand } = require("@aws-sdk/client-s3"); const client = new S3Client({}); const sourcePrefix = process.env.SRC_PREFIX; const destinationBucket = process.env.DEST_BUCKET; exports.handler = async (event, context) => { //console.log("Received event:", JSON.stringify(event, null, 2)); // Get the object from the event const bucket = event.Records[0].s3.bucket.name; const key = decodeURIComponent( event.Records[0].s3.object.key.replace(/\+/g, " ") ); if (!key.startsWith(`${sourcePrefix}year=`)) { //console.log(`Key ${key} does not start with '${sourcePrefix}year='`); return; } var copyParams = { Bucket: destinationBucket, CopySource: encodeURI(`/${bucket}/${key}`), Key: encodeURI(key), }; //console.log(`Copying object to ${copyParams.Bucket}/${copyParams.Key}`); try { const command = new CopyObjectCommand(copyParams); const response = await client.send(command); } catch (err) { console.log(err); throw new Error(err); } }; Role: Fn::GetAtt: - SESPartitionedObjectReplicationFunctionServiceRole7A64D76B - Arn Environment: Variables: SRC_PREFIX: partitioned/ DEST_BUCKET: Ref: seseventsdestinationaggregatedD1CA1006 FunctionName: SESPartitionedObjectReplicationFunction Handler: index.handler Runtime: nodejs18.x DependsOn: - SESPartitionedObjectReplicationFunctionServiceRoleDefaultPolicy265F3C9E - SESPartitionedObjectReplicationFunctionServiceRole7A64D76B BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC: 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 BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36: Type: AWS::IAM::Policy Properties: PolicyDocument: Statement: - Action: s3:PutBucketNotification Effect: Allow Resource: "*" Version: "2012-10-17" PolicyName: BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36 Roles: - Ref: BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691: Type: AWS::Lambda::Function Properties: Description: AWS CloudFormation handler for "Custom::S3BucketNotifications" resources (@aws-cdk/aws-s3) Code: ZipFile: | import boto3 # type: ignore import json import logging import urllib.request s3 = boto3.client("s3") EVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration' CONFIGURATION_TYPES = ["TopicConfigurations", "QueueConfigurations", "LambdaFunctionConfigurations"] def handler(event: dict, context): response_status = "SUCCESS" error_message = "" try: props = event["ResourceProperties"] bucket = props["BucketName"] notification_configuration = props["NotificationConfiguration"] request_type = event["RequestType"] managed = props.get('Managed', 'true').lower() == 'true' stack_id = event['StackId'] if managed: config = handle_managed(request_type, notification_configuration) else: config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration) put_bucket_notification_configuration(bucket, config) except Exception as e: logging.exception("Failed to put bucket notification configuration") response_status = "FAILED" error_message = f"Error: {str(e)}. " finally: submit_response(event, context, response_status, error_message) def handle_managed(request_type, notification_configuration): if request_type == 'Delete': return {} return notification_configuration def handle_unmanaged(bucket, stack_id, request_type, notification_configuration): external_notifications = find_external_notifications(bucket, stack_id) if request_type == 'Delete': return external_notifications def with_id(notification): notification['Id'] = f"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}" return notification notifications = {} for t in CONFIGURATION_TYPES: external = external_notifications.get(t, []) incoming = [with_id(n) for n in notification_configuration.get(t, [])] notifications[t] = external + incoming if EVENTBRIDGE_CONFIGURATION in notification_configuration: notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION] elif EVENTBRIDGE_CONFIGURATION in external_notifications: notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION] return notifications def find_external_notifications(bucket, stack_id): existing_notifications = get_bucket_notification_configuration(bucket) external_notifications = {} for t in CONFIGURATION_TYPES: external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f"{stack_id}-")] if EVENTBRIDGE_CONFIGURATION in existing_notifications: external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION] return external_notifications def get_bucket_notification_configuration(bucket): return s3.get_bucket_notification_configuration(Bucket=bucket) def put_bucket_notification_configuration(bucket, notification_configuration): s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration) def submit_response(event: dict, context, response_status: str, error_message: str): response_body = json.dumps( { "Status": response_status, "Reason": f"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}", "PhysicalResourceId": event.get("PhysicalResourceId") or event["LogicalResourceId"], "StackId": event["StackId"], "RequestId": event["RequestId"], "LogicalResourceId": event["LogicalResourceId"], "NoEcho": False, } ).encode("utf-8") headers = {"content-type": "", "content-length": str(len(response_body))} try: req = urllib.request.Request(url=event["ResponseURL"], headers=headers, data=response_body, method="PUT") with urllib.request.urlopen(req) as response: print(response.read().decode("utf-8")) print("Status code: " + response.reason) except Exception as e: print("send(..) failed executing request.urlopen(..): " + str(e)) Handler: index.handler Role: Fn::GetAtt: - BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC - Arn Runtime: python3.9 Timeout: 300 DependsOn: - BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36 - BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC SESEventsTransformationFunctionServiceRoleDCA738CD: 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 SESEventsTransformationFunction49B2A3C8: Type: AWS::Lambda::Function Properties: Code: S3Bucket: Fn::Sub: '${AWS::AccountId}-${AWS::Region}-ses-blog-utils-bucket' S3Key: TransformationLambdaCode.zip Role: Fn::GetAtt: - SESEventsTransformationFunctionServiceRoleDCA738CD - Arn FunctionName: SESEventsTransformationFunction Handler: index.lambda_handler Runtime: python3.9 Timeout: 60 DependsOn: - SESEventsTransformationFunctionServiceRoleDCA738CD KinesisFirehoseStreamServiceRole8F041D47: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: firehose.amazonaws.com Version: "2012-10-17" KinesisFirehoseStreamS3DestinationRoleC8B52E87: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: firehose.amazonaws.com Version: "2012-10-17" KinesisFirehoseStreamS3DestinationRoleDefaultPolicy8AEFFD95: Type: AWS::IAM::Policy Properties: PolicyDocument: Statement: - Action: - s3:Abort* - s3:DeleteObject* - s3:GetBucket* - s3:GetObject* - s3:List* - s3:PutObject - s3:PutObjectLegalHold - s3:PutObjectRetention - s3:PutObjectTagging - s3:PutObjectVersionTagging Effect: Allow Resource: - Fn::GetAtt: - seseventsdestinationEA24EF5F - Arn - Fn::Join: - "" - - Fn::GetAtt: - seseventsdestinationEA24EF5F - Arn - /* - Action: - logs:CreateLogStream - logs:PutLogEvents Effect: Allow Resource: Fn::GetAtt: - KinesisFirehoseStreamLogGroup75847921 - Arn - Action: lambda:InvokeFunction Effect: Allow Resource: - Fn::GetAtt: - SESEventsTransformationFunction49B2A3C8 - Arn - Fn::Join: - "" - - Fn::GetAtt: - SESEventsTransformationFunction49B2A3C8 - Arn - :* Version: "2012-10-17" PolicyName: KinesisFirehoseStreamS3DestinationRoleDefaultPolicy8AEFFD95 Roles: - Ref: KinesisFirehoseStreamS3DestinationRoleC8B52E87 KinesisFirehoseStreamLogGroup75847921: Type: AWS::Logs::LogGroup Properties: RetentionInDays: 731 UpdateReplacePolicy: Retain DeletionPolicy: Retain KinesisFirehoseStreamLogGroupS3Destination78556782: Type: AWS::Logs::LogStream Properties: LogGroupName: Ref: KinesisFirehoseStreamLogGroup75847921 UpdateReplacePolicy: Retain DeletionPolicy: Retain KinesisFirehoseStream6F9ED265: Type: AWS::KinesisFirehose::DeliveryStream Properties: DeliveryStreamEncryptionConfigurationInput: KeyType: AWS_OWNED_CMK DeliveryStreamType: DirectPut ExtendedS3DestinationConfiguration: BucketARN: Fn::GetAtt: - seseventsdestinationEA24EF5F - Arn BufferingHints: IntervalInSeconds: 60 SizeInMBs: 5 CloudWatchLoggingOptions: Enabled: true LogGroupName: Ref: KinesisFirehoseStreamLogGroup75847921 LogStreamName: Ref: KinesisFirehoseStreamLogGroupS3Destination78556782 CompressionFormat: GZIP Prefix: raw/ ProcessingConfiguration: Enabled: true Processors: - Parameters: - ParameterName: RoleArn ParameterValue: Fn::GetAtt: - KinesisFirehoseStreamS3DestinationRoleC8B52E87 - Arn - ParameterName: LambdaArn ParameterValue: Fn::GetAtt: - SESEventsTransformationFunction49B2A3C8 - Arn Type: Lambda RoleARN: Fn::GetAtt: - KinesisFirehoseStreamS3DestinationRoleC8B52E87 - Arn DependsOn: - KinesisFirehoseStreamS3DestinationRoleDefaultPolicy8AEFFD95 cfnConfigurationSet: Type: AWS::SES::ConfigurationSet Properties: Name: SESConfigurationSet KinesisPutRecordBatchPolicyA42C6A09: Type: AWS::IAM::ManagedPolicy Properties: PolicyDocument: Statement: - Action: firehose:PutRecordBatch Effect: Allow Resource: Fn::GetAtt: - KinesisFirehoseStream6F9ED265 - Arn Sid: "" Version: "2012-10-17" Description: AllowsFirehosePutRecordBatch Path: / SESRole7CABDF48: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Condition: StringEquals: AWS:SourceAccount: Ref: AWS::AccountId AWS:SourceArn: Fn::Join: - "" - - "arn:aws:ses:" - Ref: AWS::Region - ":" - Ref: AWS::AccountId - :configuration-set/SESConfigurationSet Effect: Allow Principal: Service: ses.amazonaws.com Version: "2012-10-17" ManagedPolicyArns: - Ref: KinesisPutRecordBatchPolicyA42C6A09 cfnConfigurationSetEventDestination: Type: AWS::SES::ConfigurationSetEventDestination Properties: ConfigurationSetName: SESConfigurationSet EventDestination: Enabled: true KinesisFirehoseDestination: DeliveryStreamARN: Fn::GetAtt: - KinesisFirehoseStream6F9ED265 - Arn IAMRoleARN: Fn::GetAtt: - SESRole7CABDF48 - Arn MatchingEventTypes: - send - reject - bounce - complaint - delivery - open - click - renderingFailure - subscription - deliveryDelay Name: SesBlogSolutionStack-ConfigurationSetDestination dataBrewDataset: Type: AWS::DataBrew::Dataset Properties: Input: S3InputDefinition: Bucket: Ref: seseventsdestinationEA24EF5F Key: raw/{year}/{month}/{day}/{hour}/ Name: SESDataBrewDataset Format: JSON PathOptions: LastModifiedDateCondition: Expression: relative_after :timeframe ValuesMap: - Value: -1H ValueReference: :timeframe Parameters: - DatasetParameter: CreateColumn: true Name: year Type: Number PathParameterName: year - DatasetParameter: CreateColumn: true Name: month Type: Number PathParameterName: month - DatasetParameter: CreateColumn: true Name: day Type: Number PathParameterName: day - DatasetParameter: CreateColumn: true Name: hour Type: Number PathParameterName: hour SESEventDataDatabase: Type: AWS::Glue::Database Properties: CatalogId: Ref: AWS::AccountId DatabaseInput: Name: ses_event_data_database crawlerServiceRoleF4172CF9: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: sts:AssumeRole Effect: Allow Principal: Service: glue.amazonaws.com Version: "2012-10-17" Description: A role used by Glue Crawler for data processing ManagedPolicyArns: - Fn::Join: - "" - - "arn:" - Ref: AWS::Partition - :iam::aws:policy/service-role/AWSGlueServiceRole crawlerServiceRoleDefaultPolicyB617A954: Type: AWS::IAM::Policy Properties: PolicyDocument: Statement: - Action: - s3:Abort* - s3:DeleteObject* - s3:GetBucket* - s3:GetObject* - s3:List* - s3:PutObject - s3:PutObjectLegalHold - s3:PutObjectRetention - s3:PutObjectTagging - s3:PutObjectVersionTagging Effect: Allow Resource: - Fn::GetAtt: - seseventsdestinationaggregatedD1CA1006 - Arn - Fn::Join: - "" - - Fn::GetAtt: - seseventsdestinationaggregatedD1CA1006 - Arn - /* Version: "2012-10-17" PolicyName: crawlerServiceRoleDefaultPolicyB617A954 Roles: - Ref: crawlerServiceRoleF4172CF9 SESEventDataCrawler: Type: AWS::Glue::Crawler Properties: Role: Fn::GetAtt: - crawlerServiceRoleF4172CF9 - Arn Targets: S3Targets: - Path: Fn::Join: - "" - - s3:// - Ref: seseventsdestinationaggregatedD1CA1006 - /partitioned/ DatabaseName: ses_event_data_database Name: SESEventDataCrawler RecrawlPolicy: RecrawlBehavior: CRAWL_EVERYTHING Schedule: ScheduleExpression: cron(30 0/1 * * ? *) SchemaChangePolicy: UpdateBehavior: UPDATE_IN_DATABASE AthenaResultsBucket879938FA: Type: AWS::S3::Bucket Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: aws:kms BucketName: Fn::Join: - "" - - Ref: AWS::AccountId - "-" - Ref: AWS::Region - -athena-results-location OwnershipControls: Rules: - ObjectOwnership: BucketOwnerEnforced PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true UpdateReplacePolicy: Retain DeletionPolicy: Retain AthenaResultsBucketPolicyD63DEF63: Type: AWS::S3::BucketPolicy Properties: Bucket: Ref: AthenaResultsBucket879938FA PolicyDocument: Statement: - Action: s3:* Condition: Bool: aws:SecureTransport: "false" Effect: Deny Principal: AWS: "*" Resource: - Fn::GetAtt: - AthenaResultsBucket879938FA - Arn - Fn::Join: - "" - - Fn::GetAtt: - AthenaResultsBucket879938FA - Arn - /* Version: "2012-10-17" SesAthenaWorkgroup: Type: AWS::Athena::WorkGroup Properties: Name: SesAthenaWorkgroup WorkGroupConfiguration: ResultConfiguration: EncryptionConfiguration: EncryptionOption: SSE_S3 OutputLocation: Fn::Join: - "" - - s3:// - Ref: AthenaResultsBucket879938FA