--- AWSTemplateFormatVersion: 2010-09-09 Description: AWS SaaS Factory Bootcamp Master Stack Parameters: AssetsPath: Description: S3 bucket path containing workshop resources. Type: String ParticipantAssumedRoleArn: Description: Workshop Studio Participant IAM Role. Leave blank when not using Workshop Studio. Type: String Default: '' Resources: BootcampBucket: Type: AWS::S3::Bucket BootcampBucketSsmParam: Type: AWS::SSM::Parameter Properties: Name: !Sub saas-bootcamp-bucket-${AWS::Region} Type: String Value: !Ref BootcampBucket WebsiteBucket: Type: AWS::S3::Bucket Properties: WebsiteConfiguration: IndexDocument: index.html Tags: - Key: Name Value: !Sub saas-bootcamp-website-bucket-${AWS::Region} ArtifactBucket: Type: AWS::S3::Bucket Properties: VersioningConfiguration: Status: Enabled NotificationConfiguration: EventBridgeConfiguration: EventBridgeEnabled: true Tags: - Key: Name Value: !Sub saas-bootcamp-artifact-bucket-${AWS::Region} S3ObjectsExecutionRole: Type: AWS::IAM::Role DependsOn: BootcampBucket Properties: RoleName: !Sub saas-bootcamp-s3-copy-role-${AWS::Region} Path: '/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: saas-bootcamp-s3-copy-policy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:PutLogEvents Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:*:log-stream:* - Effect: Allow Action: - logs:CreateLogStream - logs:DescribeLogStreams Resource: - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - Effect: Allow Action: - s3:GetObject Resource: - !Sub arn:aws:s3:::${AssetsPath}* - Effect: Allow Action: - s3:ListBucket - s3:ListBucketVersions - s3:GetBucketVersioning Resource: - !Sub arn:aws:s3:::${BootcampBucket} - !Sub arn:aws:s3:::${ArtifactBucket} - !Sub arn:aws:s3:::${WebsiteBucket} - Effect: Allow Action: - s3:PutObject - s3:DeleteObject - s3:DeleteObjectVersion Resource: - !Sub arn:aws:s3:::${BootcampBucket}/* - !Sub arn:aws:s3:::${ArtifactBucket}/* - !Sub arn:aws:s3:::${WebsiteBucket}/* CopyS3ObjectsLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/lambda/saas-bootcamp-s3-copy RetentionInDays: 7 CopyS3Objects: Type: AWS::Lambda::Function DependsOn: - CopyS3ObjectsLogs Properties: FunctionName: saas-bootcamp-s3-copy Role: !GetAtt S3ObjectsExecutionRole.Arn Runtime: python3.9 Timeout: 600 MemorySize: 512 Handler: index.lambda_handler Code: ZipFile: | import json import boto3 import cfnresponse from botocore.exceptions import ClientError def lambda_handler(event, context): print(json.dumps(event, default=str)) source_bucket_slash = event['ResourceProperties']['Source'] if source_bucket_slash.endswith ('/'): source_bucket = source_bucket_slash[:-1] else: source_bucket = source_bucket_slash destination_bucket = event['ResourceProperties']['Destination'] destination_region = event['ResourceProperties']['DestinationRegion'] objects_to_copy = event['ResourceProperties']['Objects'] print("Creating S3 client in destination region %s" % destination_region) s3 = boto3.client('s3', region_name=destination_region) if event['RequestType'] in ['Create', 'Update']: for key in objects_to_copy: print("Copying %s/%s -> %s/%s" % (source_bucket, key, destination_bucket, key)) try: s3.copy_object(CopySource={'Bucket':source_bucket, 'Key':key}, Bucket=destination_bucket, Key=key) except ClientError as s3_error: cfnresponse.send(event, context, cfnresponse.FAILED, {"Reason": str(s3_error)}) raise cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) elif event['RequestType'] == 'Delete': cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) else: cfnresponse.send(event, context, cfnresponse.FAILED, {"Reason": "Unknown RequestType %s" % event['RequestType']}) InvokeLambdaCopyS3Objects: Type: Custom::CustomResource DependsOn: - CopyS3ObjectsLogs Properties: ServiceToken: !GetAtt CopyS3Objects.Arn Source: !Ref AssetsPath Destination: !Ref BootcampBucket DestinationRegion: !Ref AWS::Region Objects: - resources/baseline.yml - resources/api-gateway.yml - resources/auth-service.yml - resources/tenant-service.yml - resources/user-service.yml - resources/registration-service.yml - resources/order-service.yml - resources/product-service.yml - resources/web-client.yml - source/shared-modules/config-helper/config.js - source/shared-modules/config-helper/package.json - source/shared-modules/config-helper/config/default.json - source/shared-modules/config-helper/config/development.json - source/shared-modules/config-helper/config/production.json - source/shared-modules/token-manager/token-manager.js - source/shared-modules/token-manager/package.json - source/shared-modules/dynamodb-helper/dynamodb-helper.js - source/shared-modules/dynamodb-helper/package.json - source/auth-manager/Dockerfile - source/auth-manager/server.js - source/auth-manager/package.json - source/tenant-manager/Dockerfile - source/tenant-manager/server.js - source/tenant-manager/package.json - source/user-manager/cognito-user.js - source/user-manager/Dockerfile - source/user-manager/server.js - source/user-manager/package.json - source/tenant-registration/Dockerfile - source/tenant-registration/server.js - source/tenant-registration/package.json - source/order-manager/Dockerfile - source/order-manager/server.js - source/order-manager/package.json - source/product-manager/Dockerfile - source/product-manager/server.js - source/product-manager/package.json - source/web-client/Gruntfile.js - source/web-client/app/404.html - source/web-client/app/config.json - source/web-client/app/favicon.ico - source/web-client/app/images/Auctioneer2-300px.png - source/web-client/app/images/chart.png - source/web-client/app/images/favicon.ico - source/web-client/app/images/green-circle.png - source/web-client/app/images/matt-icons_package-300px.png - source/web-client/app/images/matt-icons_package-800px.png - source/web-client/app/images/monitor.png - source/web-client/app/images/red-circle.png - source/web-client/app/images/yeoman.png - source/web-client/app/index.html - source/web-client/app/robots.txt - source/web-client/app/scripts/app.js - source/web-client/app/scripts/constants.js - source/web-client/app/scripts/controllers/confirm.js - source/web-client/app/scripts/controllers/login.js - source/web-client/app/scripts/controllers/main.js - source/web-client/app/scripts/controllers/order-add.js - source/web-client/app/scripts/controllers/order-delete.js - source/web-client/app/scripts/controllers/order-edit.js - source/web-client/app/scripts/controllers/orders.js - source/web-client/app/scripts/controllers/product-add.js - source/web-client/app/scripts/controllers/product-delete.js - source/web-client/app/scripts/controllers/product-edit.js - source/web-client/app/scripts/controllers/product-view.js - source/web-client/app/scripts/controllers/products.js - source/web-client/app/scripts/controllers/register.js - source/web-client/app/scripts/controllers/tenant-delete.js - source/web-client/app/scripts/controllers/tenant-edit.js - source/web-client/app/scripts/controllers/tenants.js - source/web-client/app/scripts/controllers/user-add.js - source/web-client/app/scripts/controllers/user-delete.js - source/web-client/app/scripts/controllers/user-edit.js - source/web-client/app/scripts/controllers/user-enable.js - source/web-client/app/scripts/controllers/users.js - source/web-client/app/styles/main.css - source/web-client/app/styles/main.scss - source/web-client/app/views/confirm.html - source/web-client/app/views/login.html - source/web-client/app/views/main.html - source/web-client/app/views/order-add.html - source/web-client/app/views/order-delete.html - source/web-client/app/views/order-edit.html - source/web-client/app/views/order-nav.html - source/web-client/app/views/orders.html - source/web-client/app/views/product-add.html - source/web-client/app/views/product-delete.html - source/web-client/app/views/product-edit.html - source/web-client/app/views/product-nav.html - source/web-client/app/views/product-view.html - source/web-client/app/views/products.html - source/web-client/app/views/register.html - source/web-client/app/views/tenant-delete.html - source/web-client/app/views/tenant-edit.html - source/web-client/app/views/tenants.html - source/web-client/app/views/user-add.html - source/web-client/app/views/user-delete.html - source/web-client/app/views/user-edit.html - source/web-client/app/views/user-enable.html - source/web-client/app/views/user-nav.html - source/web-client/app/views/users.html - source/web-client/bower.json - source/web-client/package.json ClearS3BucketLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/lambda/saas-bootcamp-s3-clear RetentionInDays: 7 ClearS3Bucket: Type: AWS::Lambda::Function Properties: FunctionName: saas-bootcamp-s3-clear Role: !GetAtt S3ObjectsExecutionRole.Arn Runtime: python3.9 Timeout: 900 MemorySize: 512 Handler: index.lambda_handler Code: ZipFile: | import json import boto3 import cfnresponse from botocore.exceptions import ClientError def lambda_handler(event, context): print(json.dumps(event, default=str)) if event['RequestType'] in ['Create', 'Update']: cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) elif event['RequestType'] == 'Delete': s3 = boto3.client('s3') bucket = event['ResourceProperties']['Bucket'] objects_to_delete = [] try: versioning_response = s3.get_bucket_versioning(Bucket=bucket) print(json.dumps(versioning_response, default=str)) if 'Status' in versioning_response and versioning_response['Status'] in ['Enabled', 'Suspended']: print("Bucket %s is versioned (%s)" % (bucket, versioning_response['Status'])) key_token = '' version_token = '' while True: if not version_token: list_response = s3.list_object_versions(Bucket=bucket, KeyMarker=key_token) else: list_response = s3.list_object_versions(Bucket=bucket, KeyMarker=key_token, VersionIdMarker=version_token) key_token = list_response['NextKeyMarker'] if 'NextKeyMarker' in list_response else '' version_token = list_response['NextVersionIdMarker'] if 'NextVersionIdMarker' in list_response else '' if 'Versions' in list_response: for s3_object in list_response['Versions']: objects_to_delete.append({'Key': s3_object['Key'], 'VersionId': s3_object['VersionId']}) if not list_response['IsTruncated']: break else: print("Bucket %s is not versioned" % bucket) token = '' while True: if not token: list_response = s3.list_objects_v2(Bucket=bucket) else: list_response = s3.list_objects_v2(Bucket=bucket, ContinuationToken=token) token = list_response['NextContinuationToken'] if 'NextContinuationToken' in list_response else '' if 'Contents' in list_response: for s3_object in list_response['Contents']: objects_to_delete.append({'Key': s3_object['Key']}) if not list_response['IsTruncated']: break if len(objects_to_delete) > 0: print("Deleting %d objects" % len(objects_to_delete)) max_batch_size = 1000 batch_start = 0 batch_end = 0 while batch_end < len(objects_to_delete): batch_start = batch_end batch_end += max_batch_size if (batch_end > len(objects_to_delete)): batch_end = len(objects_to_delete) delete_response = s3.delete_objects(Bucket=bucket, Delete={'Objects': objects_to_delete[batch_start:batch_end]}) print("Cleaned up %d objects in bucket %s" % (len(delete_response['Deleted']), bucket)) else: print("Bucket %s is empty. Nothing to clean up." % bucket) # Tell CloudFormation we're all done cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) except ClientError as s3_error: cfnresponse.send(event, context, cfnresponse.FAILED, {"Reason": str(s3_error)}) raise else: cfnresponse.send(event, context, cfnresponse.FAILED, {"Reason": "Unknown RequestType %s" % event['RequestType']}) InvokeClearS3BucketWorkshopS3Bucket: Type: Custom::CustomResource DependsOn: - ClearS3BucketLogs Properties: ServiceToken: !GetAtt ClearS3Bucket.Arn Bucket: !Ref BootcampBucket InvokeLambdaClearArtifactBucket: Type: Custom::CustomResource DependsOn: - ClearS3BucketLogs Properties: ServiceToken: !GetAtt ClearS3Bucket.Arn Bucket: !Ref ArtifactBucket InvokeLambdaClearWebsiteBucket: Type: Custom::CustomResource DependsOn: - ClearS3BucketLogs Properties: ServiceToken: !GetAtt ClearS3Bucket.Arn Bucket: !Ref WebsiteBucket Baseline: Type: AWS::CloudFormation::Stack DependsOn: InvokeLambdaCopyS3Objects Properties: TemplateURL: !Sub https://s3.${AWS::Region}.amazonaws.com/${BootcampBucket}/resources/baseline.yml Parameters: ParticipantAssumedRoleArn: !Ref ParticipantAssumedRoleArn BootcampBucket: !Ref BootcampBucket ArtifactBucket: !Ref ArtifactBucket WebsiteBucket: !Ref WebsiteBucket Outputs: SaaSFactoryBootcampStack: Description: Saas Bootcamp CloudFormation stack Value: !Ref AWS::StackName ApiGatewayEndpoint: Description: API Gateway Invoke URL Value: !GetAtt Baseline.Outputs.ApiGatewayEndpoint WebAppUrl: Description: Web application URL Value: !GetAtt Baseline.Outputs.WebAppUrl ParticipantAssumedRoleArn: Description: ParticipantAssumedRoleArn Value: !Ref ParticipantAssumedRoleArn ...