--- # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy of this # software and associated documentation files (the "Software"), to deal in the Software # without restriction, including without limitation the rights to use, copy, modify, # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. AWSTemplateFormatVersion: '2010-09-09' Description: AWS SaaS Factory Multi-Tenant RDBMS Data Isolation Using PostgreSQL Row Level Security Parameters: KeyPair: Description: Amazon EC2 Key Pair for a Jump Box with access to the database #Type: AWS::EC2::KeyPair::KeyName Type: String DBName: Description: RDS Database Name Type: String MinLength: 3 MaxLength: 31 AllowedPattern: ^[a-zA-Z]+[a-zA-Z0-9_\$]*$ ConstraintDescription: Database name must be between 3 and 31 characters in length DBMasterUsername: Description: RDS Master Username Type: String DBMasterPassword: Description: RDS Master User Password Type: String NoEcho: true MinLength: 8 AllowedPattern: ^[a-zA-Z0-9/@"' ]{8,}$ ConstraintDescription: RDS passwords must be at least 8 characters in length DBAppUsername: Description: RDS Application Username Type: String DBAppPassword: Description: RDS Application User Password Type: String NoEcho: true MinLength: 8 AllowedPattern: ^[a-zA-Z0-9/@"' ]{8,}$ ConstraintDescription: RDS passwords must be at least 8 characters in length RDSInstanceType: Description: RDS Instance Type Type: String Default: db.t2.micro RDSEngineVersion: Description: PostgreSQL version Type: String Default: 12 AMI: Description: EC2 Image ID for the Jump Box (don't change) Type: AWS::SSM::Parameter::Value Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: EC2 Configuration Parameters: - KeyPair - AMI - Label: default: Database Configuration Parameters: - RDSInstanceType - RDSEngineVersion - DBName - DBMasterUsername - DBMasterPassword - DBAppUsername - DBAppPassword ParameterLabels: KeyPair: default: Key Pair for the Jump box DBName: default: Database Name DBMasterUsername: default: RDS Master Username DBMasterPassword: default: RDS Master Password DBAppUsername: default: RDS Application Username DBAppPassword: default: RDS Application Password AMI: default: Do Not Change - Jump Box AMI Conditions: HasKeyPair: !Not [!Equals [!Ref KeyPair, '']] Resources: VPC: Type: AWS::EC2::VPC Properties: CidrBlock: 10.0.0.0/16 EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: saas-factory-pg-rls-vpc InternetGateway: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: saas-factory-pg-rls-igw AttachGateway: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref VPC InternetGatewayId: !Ref InternetGateway RouteTablePublic: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: saas-factory-pg-rls-route-pub RoutePublic: Type: AWS::EC2::Route DependsOn: AttachGateway Properties: RouteTableId: !Ref RouteTablePublic DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway SubnetPublicA: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [0, !GetAZs ''] CidrBlock: 10.0.32.0/19 Tags: - Key: Name Value: saas-factory-pg-rls-subA-pub SubnetPublicARouteTable: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref SubnetPublicA RouteTableId: !Ref RouteTablePublic SubnetPublicB: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [1, !GetAZs ''] CidrBlock: 10.0.96.0/19 Tags: - Key: Name Value: saas-factory-pg-rls-subB-pub SubnetPublicBRouteTable: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref SubnetPublicB RouteTableId: !Ref RouteTablePublic NatGatewayAddrA: Type: AWS::EC2::EIP DependsOn: AttachGateway Properties: Domain: vpc NatGatewayA: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGatewayAddrA.AllocationId SubnetId: !Ref SubnetPublicA Tags: - Key: Name Value: saas-factory-pg-rls-nat-subA-pub RouteTableNatA: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: saas-factory-pg-rls-route-natA RouteNatA: Type: AWS::EC2::Route DependsOn: NatGatewayA Properties: RouteTableId: !Ref RouteTableNatA DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGatewayA SubnetPrivateA: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [0, !GetAZs ''] CidrBlock: 10.0.0.0/19 Tags: - Key: Name Value: saas-factory-pg-rls-subA-priv SubnetPrivateARouteTable: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref SubnetPrivateA RouteTableId: !Ref RouteTableNatA NatGatewayAddrB: Type: AWS::EC2::EIP DependsOn: AttachGateway Properties: Domain: vpc NatGatewayB: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGatewayAddrB.AllocationId SubnetId: !Ref SubnetPublicB Tags: - Key: Name Value: saas-factory-pg-rls-nat-subB-pub RouteTableNatB: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: saas-factory-pg-rls-route-natB RouteNatB: Type: AWS::EC2::Route DependsOn: NatGatewayB Properties: RouteTableId: !Ref RouteTableNatB DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGatewayB SubnetPrivateB: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [1, !GetAZs ''] CidrBlock: 10.0.64.0/19 Tags: - Key: Name Value: saas-factory-pg-rls-subB-priv SubnetPrivateBRouteTable: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref SubnetPrivateB RouteTableId: !Ref RouteTableNatB JumpBoxSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: saas-factory-pg-rls-ec2-sg GroupDescription: Jump Box SSH Security Group SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: 0.0.0.0/0 VpcId: !Ref VPC Tags: - Key: Name Value: saas-factory-pg-rls-ec2-sg JumpBox: Type: AWS::EC2::Instance Condition: HasKeyPair DependsOn: JumpBoxSecurityGroup Metadata: AWS::CloudFormation::Init: configSets: Setup: - Configure Configure: packages: yum: postgresql: [] commands: yum_update: command: yum update -y Properties: ImageId: !Ref AMI InstanceType: t2.micro KeyName: !Ref KeyPair NetworkInterfaces: - AssociatePublicIpAddress: true DeviceIndex: 0 SubnetId: !Ref SubnetPublicA GroupSet: - !Ref JumpBoxSecurityGroup Tags: - Key: Name Value: saas-factory-pg-rls-jumpbox UserData: Fn::Base64: !Join - '' - - "#!/bin/bash -xe\n" - "yum update -y aws-cfn-bootstrap\n" - "# Run the config sets from the CloudFormation metadata\n" - "/opt/aws/bin/cfn-init -v -s " - !Ref AWS::StackName - " -r JumpBox -c Setup --region " - !Ref AWS::Region - "\n\n" LambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: saas-factory-pg-rls-cfn-lambda-role Path: '/' AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: saas-factory-pg-rls-cfn-lambda-policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogStream - logs:PutLogEvents Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:* - Effect: Allow Action: - logs:DescribeLogStreams Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - Effect: Allow Action: - ssm:PutParameter - ssm:GetParameter - ssm:DeleteParameter Resource: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:* - Effect: Allow Action: - kms:Encrypt - kms:Decrypt - kms:ListKeys - kms:ListAliases - kms:Describe* Resource: !Sub arn:aws:kms:${AWS::Region}:${AWS::AccountId}:* - Effect: Allow Action: - ec2:CreateNetworkInterface - ec2:DescribeNetworkInterfaces - ec2:DeleteNetworkInterface Resource: '*' - Effect: Allow Action: - codebuild:StartBuild Resource: !Sub arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/* - Effect: Allow Action: - s3:ListBucket - s3:ListBucketVersions - s3:GetBucketVersioning Resource: - !Sub arn:aws:s3:::${CodePipelineBucket} - !Sub arn:aws:s3:::${CloudTrailBucket} - Effect: Allow Action: - s3:PutObject - s3:DeleteObject - s3:DeleteObjectVersion Resource: - !Sub arn:aws:s3:::${CodePipelineBucket}/* - !Sub arn:aws:s3:::${CloudTrailBucket}/* - Effect: Allow Action: - ecr:DescribeImages - ecr:BatchDeleteImage Resource: - !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/saas-factory-pg-rls LambdaSSMPutParamSecureLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/saas-factory-pg-rls-ssm-secure RetentionInDays: 14 LambdaSSMPutParamSecure: Type: AWS::Lambda::Function DependsOn: LambdaSSMPutParamSecureLogs Properties: FunctionName: !Sub saas-factory-pg-rls-ssm-secure Role: !GetAtt LambdaExecutionRole.Arn Runtime: python3.7 Timeout: 300 MemorySize: 256 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)) ssm = boto3.client('ssm') parameter_name = event['ResourceProperties']['Name'] parameter_value = event['ResourceProperties']['Value'] if event['RequestType'] in ['Create', 'Update']: try: put_response = ssm.put_parameter(Name=parameter_name, Value=parameter_value, Type='SecureString', Overwrite=True) cfnresponse.send(event, context, cfnresponse.SUCCESS, {"Version": put_response['Version']}) except ClientError as ssm_error: print("ssm error %s" % str(ssm_error)) cfnresponse.send(event, context, cfnresponse.FAILED, {"Reason": str(ssm_error)}) raise elif event['RequestType'] == 'Delete': try: delete_response = ssm.delete_parameter(Name=parameter_name) cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) except ssm.exceptions.ParameterNotFound as not_found: # Ignore parameter not found cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) except ClientError as ssm_error: print("ssm error %s" % str(ssm_error)) cfnresponse.send(event, context, cfnresponse.FAILED, {"Reason": str(ssm_error)}) raise else: cfnresponse.send(event, context, cfnresponse.FAILED, {"Reason": "Unknown RequestType %s" % event['RequestType']}) InvokeLambdaSSMPutParamSecure: Type: Custom::CustomResource DependsOn: LambdaSSMPutParamSecureLogs Properties: ServiceToken: !GetAtt LambdaSSMPutParamSecure.Arn Name: saas-factory-pg-rls-owner-pw # SSM Parameter Name Value: !Ref DBMasterPassword InvokeLambdaSSMPutParamSecure2: Type: Custom::CustomResource DependsOn: LambdaSSMPutParamSecureLogs Properties: ServiceToken: !GetAtt LambdaSSMPutParamSecure.Arn Name: saas-factory-pg-rls-app-pw Value: !Ref DBAppPassword LambdaClearEcrImagesLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/saas-factory-pg-rls-clear-ecr RetentionInDays: 7 LambdaClearEcrImages: Type: AWS::Lambda::Function DependsOn: LambdaClearEcrImagesLogs Properties: FunctionName: !Sub saas-factory-pg-rls-clear-ecr Role: !GetAtt LambdaExecutionRole.Arn Runtime: python3.7 Timeout: 300 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': ecr = boto3.client('ecr') repo = event['ResourceProperties']['Repository'] try: images = [] token = None while True: if not token: images_response = ecr.describe_images(repositoryName=repo, maxResults=1000) else: images_response = ecr.describe_images(repositoryName=repo, nextToken=token, maxResults=1000) token = images_response['nextToken'] if 'nextToken' in images_response else '' if 'imageDetails' in images_response: for image_detail in images_response['imageDetails']: images.append({"imageDigest": image_detail['imageDigest']}) if not token: break print("Deleting %d images from repo %s" % (len(images), repo)) if len(images) > 0: delete_response = ecr.batch_delete_image(repositoryName=repo, imageIds=images) if 'failures' in delete_response and len(delete_response['failures']) > 0: for fail in delete_response['failures']: print("Failed to delete image %s %s in repo %s" % (fail['imageId']['imageDigest'], fail['failureReason'], repo)) cfnresponse.send(event, context, cfnresponse.FAILED, {"Reason": "ecr:BatchDeleteImage failed"}) else: cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) else: cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) except ClientError as ecr_error: print("ecr error %s" % str(ecr_error)) raise else: cfnresponse.send(event, context, cfnresponse.FAILED, {"Reason": "Unknown RequestType %s" % event['RequestType']}) RDSSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: saas-factory-pg-rls-rds-sg GroupDescription: RDS Aurora PostgreSQL 5432 Security Group VpcId: !Ref VPC Tags: - Key: Name Value: saas-factory-pg-rls-rds-sg RDSSecurityGroupIngressJumpBox: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: !Ref RDSSecurityGroup IpProtocol: tcp FromPort: 5432 ToPort: 5432 SourceSecurityGroupId: !Ref JumpBoxSecurityGroup RDSSecurityGroupIngressECS: Type: AWS::EC2::SecurityGroupIngress DependsOn: ECSSecurityGroup Properties: GroupId: !Ref RDSSecurityGroup IpProtocol: tcp FromPort: 5432 ToPort: 5432 SourceSecurityGroupId: !Ref ECSSecurityGroup RDSSubnetGroup: Type: AWS::RDS::DBSubnetGroup Properties: DBSubnetGroupDescription: saas-factory-pg-rls-rds-subnets DBSubnetGroupName: saas-factory-pg-rls-rds-subnets SubnetIds: - !Ref SubnetPrivateA - !Ref SubnetPrivateB RDSInstance: Type: AWS::RDS::DBInstance DependsOn: RDSSecurityGroup DeletionPolicy: Delete Properties: DBInstanceIdentifier: saas-factory-pg-rls-rds-instance DBInstanceClass: !Ref RDSInstanceType VPCSecurityGroups: - !Ref RDSSecurityGroup DBSubnetGroupName: !Ref RDSSubnetGroup DeleteAutomatedBackups: true MultiAZ: false Engine: postgres EngineVersion: !Ref RDSEngineVersion DBName: !Ref DBName MasterUsername: !Ref DBMasterUsername MasterUserPassword: Fn::Join: - '' - - '{{resolve:ssm-secure:saas-factory-pg-rls-owner-pw:' - !GetAtt InvokeLambdaSSMPutParamSecure.Version - '}}' AllocatedStorage: 20 StorageType: gp2 ECSRepository: Type: AWS::ECR::Repository Properties: RepositoryName: saas-factory-pg-rls InvokeClearEcrRepoImages: Type: Custom::CustomResource Properties: ServiceToken: !GetAtt LambdaClearEcrImages.Arn Repository: !Ref ECSRepository ECSCluster: Type: AWS::ECS::Cluster Properties: ClusterName: saas-factory-pg-rls ECSTaskExecutionRole: Type: AWS::IAM::Role Properties: RoleName: saas-factory-pg-rls-ecs-task-exec-role Path: '/' AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - ecs-tasks.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: saas-factory-pg-rls-ecs-task-exec-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 Resource: - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:* - Effect: Allow Action: - ecr:BatchCheckLayerAvailability - ecr:GetDownloadUrlForLayer - ecr:BatchGetImage Resource: - !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${ECSRepository} - Effect: Allow Action: - ecr:GetAuthorizationToken Resource: '*' - Effect: Allow Action: - ssm:GetParameters Resource: - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/saas-factory-pg-rls-owner-pw - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/saas-factory-pg-rls-app-pw ECSLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: '/ecs/saas-factory-pg-rls-app' RetentionInDays: 14 ECSTaskRole: Type: AWS::IAM::Role Properties: RoleName: saas-factory-pg-rls-app-task-role Path: '/' AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - ecs-tasks.amazonaws.com Action: - sts:AssumeRole ECSTaskDefinition: Type: AWS::ECS::TaskDefinition Properties: Family: saas-factory-pg-rls-app ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn TaskRoleArn: !GetAtt ECSTaskRole.Arn RequiresCompatibilities: - FARGATE Memory: 1024 Cpu: 512 NetworkMode: awsvpc ContainerDefinitions: - Name: saas-factory-pg-rls-app Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECSRepository}:latest PortMappings: - ContainerPort: 8080 LogConfiguration: LogDriver: awslogs Options: awslogs-group: !Ref ECSLogGroup awslogs-region: !Ref AWS::Region awslogs-stream-prefix: ecs Environment: - Name: AWS_REGION Value: !Ref AWS::Region - Name: DB_HOST Value: !GetAtt RDSInstance.Endpoint.Address - Name: DB_NAME Value: !Ref DBName - Name: DB_USER Value: !Ref DBAppUsername - Name: DB_ADMIN_USER Value: !Ref DBMasterUsername Secrets: - Name: DB_ADMIN_PASS ValueFrom: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/saas-factory-pg-rls-owner-pw - Name: DB_PASS ValueFrom: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/saas-factory-pg-rls-app-pw ECSSecurityGroup: Type: AWS::EC2::SecurityGroup DependsOn: VPC Properties: GroupName: saas-factory-pg-rls-ecs-sg GroupDescription: Access to Fargate Containers VpcId: !Ref VPC ECSSecurityGroupIngress: Type: AWS::EC2::SecurityGroupIngress DependsOn: - ECSSecurityGroup - ALBSecurityGroup Properties: GroupId: !Ref ECSSecurityGroup SourceSecurityGroupId: !Ref ALBSecurityGroup IpProtocol: -1 Tags: - Key: Name Value: saas-factory-pg-rls-ecs-sg ALBSecurityGroup: Type: AWS::EC2::SecurityGroup DependsOn: VPC Properties: GroupName: saas-factory-pg-rls-alb-sg GroupDescription: Access to the load balancer VpcId: !Ref VPC SecurityGroupIngress: - CidrIp: 0.0.0.0/0 IpProtocol: tcp FromPort: 80 ToPort: 80 ECSLoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Scheme: internet-facing LoadBalancerAttributes: - Key: idle_timeout.timeout_seconds Value: 30 Subnets: - !Ref SubnetPublicA - !Ref SubnetPublicB SecurityGroups: [!Ref ALBSecurityGroup] ALBTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: Name: saas-factory-pg-rls-target-group HealthCheckProtocol: HTTP HealthCheckPath: '/health' HealthCheckIntervalSeconds: 30 HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 2 UnhealthyThresholdCount: 2 Port: 8080 Protocol: HTTP TargetType: ip VpcId: !Ref VPC TargetGroupAttributes: - Key: stickiness.enabled Value: 'true' - Key: stickiness.type Value: lb_cookie - Key: stickiness.lb_cookie.duration_seconds Value: '86400' ALBListener: Type: AWS::ElasticLoadBalancingV2::Listener DependsOn: ECSLoadBalancer Properties: LoadBalancerArn: !Ref ECSLoadBalancer DefaultActions: - TargetGroupArn: !Ref ALBTargetGroup Type: forward Port: 80 Protocol: HTTP ALBRule: Type: AWS::ElasticLoadBalancingV2::ListenerRule Properties: Actions: - TargetGroupArn: !Ref ALBTargetGroup Type: forward Conditions: - Field: path-pattern Values: ['*'] ListenerArn: !Ref ALBListener Priority: 1 ECSService: Type: AWS::ECS::Service DependsOn: - ECSTaskDefinition - ALBRule - InvokeLambdaCodeBuildStartBuild Properties: ServiceName: saas-factory-pg-rls-app Cluster: !Ref ECSCluster TaskDefinition: !Ref ECSTaskDefinition LaunchType: FARGATE DesiredCount: 1 NetworkConfiguration: AwsvpcConfiguration: SecurityGroups: - !Ref ECSSecurityGroup Subnets: - !Ref SubnetPrivateA - !Ref SubnetPrivateB LoadBalancers: - ContainerName: saas-factory-pg-rls-app ContainerPort: 8080 TargetGroupArn: !Ref ALBTargetGroup CodeBuildRole: Type: AWS::IAM::Role Properties: RoleName: saas-factory-pg-rls-cfn-codebuild-role Path: '/' AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - codebuild.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: saas-factory-pg-rls-cfn-codebuild-policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:* - Effect: Allow Action: - ecr:GetAuthorizationToken Resource: '*' - Effect: Allow Action: - s3:PutObject - s3:GetObject - s3:GetObjectVersion - s3:GetBucketVersioning Resource: - !Sub arn:aws:s3:::${CodePipelineBucket} - !Sub arn:aws:s3:::${CodePipelineBucket}/* - Effect: Allow Action: - ecr:GetDownloadUrlForLayer - ecr:BatchGetImage - ecr:BatchCheckLayerAvailability - ecr:PutImage - ecr:InitiateLayerUpload - ecr:UploadLayerPart - ecr:CompleteLayerUpload Resource: !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${ECSRepository} ClearS3BucketLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/saas-factory-pg-rls-s3-clear RetentionInDays: 30 ClearS3Bucket: Type: AWS::Lambda::Function DependsOn: ClearS3BucketLogs Properties: FunctionName: !Sub saas-factory-pg-rls-s3-clear Role: !GetAtt LambdaExecutionRole.Arn Runtime: python3.7 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']}) InvokeClearS3BucketCodePipelineBucket: Type: Custom::CustomResource DependsOn: - ClearS3Bucket - CodePipelineBucket - ClearS3BucketLogs Properties: ServiceToken: !GetAtt ClearS3Bucket.Arn Bucket: !Ref CodePipelineBucket InvokeClearS3BucketCloudTrailBucket: Type: Custom::CustomResource DependsOn: - ClearS3Bucket - CloudTrailBucket - ClearS3BucketLogs Properties: ServiceToken: !GetAtt ClearS3Bucket.Arn Bucket: !Ref CloudTrailBucket CodePipelineBucket: Type: AWS::S3::Bucket Properties: VersioningConfiguration: Status: Enabled Tags: - Key: Name Value: saas-factory-pg-rls-pipeline-bucket CloudTrailBucket: Type: AWS::S3::Bucket Properties: Tags: - Key: Name Value: saas-factory-pg-rls-cloudtrail-bucket CloudTrailBucketPolicy: Type: AWS::S3::BucketPolicy DependsOn: CloudTrailBucket Properties: Bucket: Ref: CloudTrailBucket PolicyDocument: Version: '2012-10-17' Statement: - Sid: AWSCloudTrailAclCheck20150319 Effect: Allow Principal: Service: cloudtrail.amazonaws.com Action: s3:GetBucketAcl Resource: !GetAtt CloudTrailBucket.Arn - Sid: AWSCloudTrailWrite20150319 Effect: Allow Principal: Service: cloudtrail.amazonaws.com Action: s3:PutObject Resource: !Sub arn:aws:s3:::${CloudTrailBucket}/AWSLogs/${AWS::AccountId}/* Condition: StringEquals: s3:x-amz-acl: bucket-owner-full-control CloudTrailForCodePipelineTrigger: Type: AWS::CloudTrail::Trail DependsOn: CloudTrailBucketPolicy Properties: TrailName: saas-factory-pg-rls-codebuild-trail S3BucketName: !Ref CloudTrailBucket IsLogging: true EventSelectors: - IncludeManagementEvents: false ReadWriteType: WriteOnly DataResources: - Type: AWS::S3::Object Values: - !Sub arn:aws:s3:::${CodePipelineBucket}/saas-factory-pg-rls-app CloudWatchEventRoleForCloudTrail: Type: AWS::IAM::Role Properties: RoleName: saas-factory-pg-rls-cfn-cloudwatch-event-role Path: '/' AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - events.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: saas-factory-pg-rls-cfn-cloudwatch-event-policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - codepipeline:StartPipelineExecution Resource: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${CodePipelineDeploy} CloudWatchEventRuleForCodePipeline: Type: AWS::Events::Rule DependsOn: CloudWatchEventRoleForCloudTrail Properties: EventPattern: source: - aws.s3 detail-type: - 'AWS API Call via CloudTrail' detail: eventSource: - s3.amazonaws.com eventName: - CopyObject - PutObject - CompleteMultipartUpload requestParameters: bucketName: - !Ref CodePipelineBucket key: - saas-factory-pg-rls-app Targets: - Arn: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${CodePipelineDeploy} RoleArn: !GetAtt CloudWatchEventRoleForCloudTrail.Arn Id: saas-factory-pg-rls-app-deploy CodeBuildProject: Type: AWS::CodeBuild::Project Properties: Name: saas-factory-pg-rls-app Tags: - Key: Name Value: saas-factory-pg-rls-app ServiceRole: !GetAtt CodeBuildRole.Arn TimeoutInMinutes: 10 Artifacts: Type: S3 Location: !Ref CodePipelineBucket Path: '/' Name: saas-factory-pg-rls-app Packaging: ZIP Environment: ComputeType: BUILD_GENERAL1_SMALL Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0 Type: LINUX_CONTAINER PrivilegedMode: true EnvironmentVariables: - Name: REPOSITORY_URI Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECSRepository} Source: Type: NO_SOURCE BuildSpec: | version: 0.2 phases: install: runtime-versions: java: corretto11 pre_build: commands: - mkdir pgrls - cd pgrls - git init - git pull https://github.com/aws-samples/aws-saas-factory-postgresql-rls.git - cd ../ build: commands: - cd pgrls/app - mvn clean package - docker image build -t pgrls -f Dockerfile . - docker tag pgrls:latest "${REPOSITORY_URI}:latest" - cd ../../ post_build: commands: - aws ecr get-login --no-include-email --region $AWS_REGION | awk '{print $6}' | docker login -u AWS --password-stdin $REPOSITORY_URI - docker push "${REPOSITORY_URI}:latest" - printf '[{"name":"saas-factory-pg-rls-app","imageUri":"%s"}]' ${REPOSITORY_URI}:latest > imagedefinitions.json artifacts: files: imagedefinitions.json discard-paths: yes CodePipelineRole: Type: AWS::IAM::Role DependsOn: CodePipelineBucket Properties: RoleName: saas-factory-pg-rls-cfn-codepipeline-role Path: '/' AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - codepipeline.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonECS_FullAccess Policies: - PolicyName: saas-factory-pg-rls-cfn-codepipeline-policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - iam:PassRole Resource: '*' Condition: StringEqualsIfExists: iamPassedToService: - ecs-tasks.amazonaws.com - Effect: Allow Action: - s3:PutObject - s3:GetObject - s3:GetObjectVersion - s3:GetBucketVersioning Resource: - !Sub arn:aws:s3:::${CodePipelineBucket} - !Sub arn:aws:s3:::${CodePipelineBucket}/* CodePipelineDeploy: Type: AWS::CodePipeline::Pipeline Properties: Name: saas-factory-pg-rls-app-deploy RoleArn: !GetAtt CodePipelineRole.Arn ArtifactStore: Location: !Ref CodePipelineBucket Type: S3 Stages: - Name: Source Actions: - Name: SourceAction ActionTypeId: Category: Source Owner: AWS Provider: S3 Version: 1 Configuration: S3Bucket: !Ref CodePipelineBucket S3ObjectKey: saas-factory-pg-rls-app PollForSourceChanges: false OutputArtifacts: - Name: imgdef - Name: Deploy Actions: - Name: DeployAction ActionTypeId: Category: Deploy Owner: AWS Provider: ECS Version: 1 Configuration: ClusterName: !Ref ECSCluster ServiceName: saas-factory-pg-rls-app FileName: imagedefinitions.json InputArtifacts: - Name: imgdef LambdaCodeBuildStartBuildLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/saas-factory-pg-rls-codebuild-start RetentionInDays: 14 LambdaCodeBuildStartBuild: Type: AWS::Lambda::Function DependsOn: LambdaCodeBuildStartBuildLogs Properties: FunctionName: !Sub saas-factory-pg-rls-codebuild-start Role: !GetAtt LambdaExecutionRole.Arn Runtime: python3.7 Timeout: 60 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'] == 'Create': try: codebuild = boto3.client('codebuild') response = codebuild.start_build(projectName = event['ResourceProperties']['Project']) cfnresponse.send(event, context, cfnresponse.SUCCESS, {"BuildStatus": response['build']['buildStatus']}) except ClientError as codebuild_error: cfnresponse.send(event, context, cfnresponse.FAILED, {"Reason": str(codebuild_error)}) raise elif event['RequestType'] in ['Update', 'Delete']: cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) else: cfnresponse.send(event, context, cfnresponse.FAILED, {"Reason": "Unknown RequestType %s" % event['RequestType']}) InvokeLambdaCodeBuildStartBuild: Type: Custom::CustomResource DependsOn: - CodeBuildProject - ECSRepository - LambdaCodeBuildStartBuildLogs Properties: ServiceToken: !GetAtt LambdaCodeBuildStartBuild.Arn Project: saas-factory-pg-rls-app Outputs: LoadBalancerEndpoint: Description: Load balancer URL Value: !GetAtt ECSLoadBalancer.DNSName RDSEndpoint: Description: RDS Endpoint Value: !GetAtt RDSInstance.Endpoint.Address RDSDatabaseName: Description: Database Name Value: !Ref DBName RDSDatabaseMasterUser: Description: Master Database User Value: !Ref DBMasterUsername RDSDatabaseAppUser: Description: Application Database User Value: !Ref DBAppUsername JumpBoxDNS: Condition: HasKeyPair Description: Jump Box DNS Value: !GetAtt JumpBox.PublicDnsName ...