diff --git a/dist/cloudformation-stack-ttl.yml b/dist/cloudformation-stack-ttl.yml new file mode 100644 index 00000000..34105833 --- /dev/null +++ b/dist/cloudformation-stack-ttl.yml @@ -0,0 +1,179 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: Schedule automatic deletion of CloudFormation stacks +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: Input configuration + Parameters: + - StackName + - TTL + ParameterLabels: + StackName: + default: Stack name + TTL: + default: Time-to-live +Parameters: + BUILDID: + Type: String + Default: '' + StackName: + Type: String + Description: Stack name that will be deleted. + TTL: + Type: Number + Description: Time-to-live in minutes for the stack. +Resources: + DeleteCFNLambdaExecutionRole: + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: ["lambda.amazonaws.com"] + Action: "sts:AssumeRole" + Path: "/" + Policies: + - PolicyName: !Sub "DeleteCFNLambdaExecutionRole-${StackName}" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Action: + - "logs:CreateLogGroup" + - "logs:CreateLogStream" + - "logs:PutLogEvents" + Resource: "arn:aws:logs:*:*:*" + - Effect: "Allow" + Action: + - "cloudformation:DeleteStack" + Resource: !Sub "arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${StackName}/*" + DeleteCFNLambda: + Type: "AWS::Lambda::Function" + DependsOn: + - DeleteCFNLambdaExecutionRole + Properties: + FunctionName: !Sub "DeleteCFNLambda-${StackName}" + Code: + ZipFile: | + import boto3 + import os + import json + + stack_name = os.environ['stackName'] + + def delete_cfn(stack_name): + try: + cfn = boto3.resource('cloudformation') + stack = cfn.Stack(stack_name) + stack.delete() + return "SUCCESS" + except: + return "ERROR" + + def handler(event, context): + print("Received event:") + print(json.dumps(event)) + return delete_cfn(stack_name) + Environment: + Variables: + stackName: !Ref 'StackName' + Handler: "index.handler" + Runtime: "python3.6" + Timeout: "5" + Role: !GetAtt DeleteCFNLambdaExecutionRole.Arn + DeleteStackEventRule: + DependsOn: + - DeleteCFNLambda + - GenerateCronExpression + Type: "AWS::Events::Rule" + Properties: + Name: !Join [ "", [ 'DeleteStackEventRule', !Ref BUILDID ] ] + Description: Delete stack event + ScheduleExpression: !GetAtt GenerateCronExpression.cron_exp + State: "ENABLED" + Targets: + - + Arn: !GetAtt DeleteCFNLambda.Arn + Id: 'DeleteCFNLambda' + PermissionForDeleteCFNLambda: + Type: "AWS::Lambda::Permission" + Properties: + FunctionName: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:DeleteCFNLambda-${StackName}" + Action: "lambda:InvokeFunction" + Principal: "events.amazonaws.com" + SourceArn: !GetAtt DeleteStackEventRule.Arn + BasicLambdaExecutionRole: + Type: "AWS::IAM::Role" + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: ["lambda.amazonaws.com"] + Action: "sts:AssumeRole" + Path: "/" + Policies: + - PolicyName: !Sub "BasicLambdaExecutionRole-${StackName}" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Action: + - "logs:CreateLogGroup" + - "logs:CreateLogStream" + - "logs:PutLogEvents" + Resource: "arn:aws:logs:*:*:*" + GenerateCronExpLambda: + Type: "AWS::Lambda::Function" + Properties: + FunctionName: !Join [ "", [ 'GenerateCronExpLambda', !Ref BUILDID ] ] + Code: + ZipFile: | + from datetime import datetime, timedelta + import os + import logging + import json + import cfnresponse + + def deletion_time(ttl): + delete_at_time = datetime.now() + timedelta(minutes=int(ttl)) + hh = delete_at_time.hour + mm = delete_at_time.minute + yyyy = delete_at_time.year + month = delete_at_time.month + dd = delete_at_time.day + # minutes hours day month day-of-week year + cron_exp = "cron({} {} {} {} ? {})".format(mm, hh, dd, month, yyyy) + return cron_exp + + def handler(event, context): + print('Received event: %s' % json.dumps(event)) + status = cfnresponse.SUCCESS + try: + if event['RequestType'] == 'Delete': + cfnresponse.send(event, context, status, {}) + else: + ttl = event['ResourceProperties']['ttl'] + responseData = {} + responseData['cron_exp'] = deletion_time(ttl) + cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) + except Exception as e: + logging.error('Exception: %s' % e, exc_info=True) + status = cfnresponse.FAILED + cfnresponse.send(event, context, status, {}, None) + Handler: "index.handler" + Runtime: "python3.6" + Timeout: "5" + Role: !GetAtt BasicLambdaExecutionRole.Arn + + GenerateCronExpression: + Type: "Custom::GenerateCronExpression" + Version: "1.0" + Properties: + Name: !Sub "GenerateCronExpression-${StackName}" + ServiceToken: !GetAtt GenerateCronExpLambda.Arn + ttl: !Ref 'TTL' diff --git a/dist/task-def-formation.yml b/dist/task-def-formation.yml new file mode 100644 index 00000000..c2d3a4e7 --- /dev/null +++ b/dist/task-def-formation.yml @@ -0,0 +1,322 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: >- + AWS Fargate cluster that can span public and private subnets. Supports public + facing load balancers, private internal load balancers, and both internal and + external service discovery namespaces. +Parameters: + EnvironmentName: + Type: String + Default: development + Description: 'Your deployment environment: DEV, QA , PROD' + ServiceName: + Type: String + Default: example + Description: A name for the service + ImageUrl: + Type: String + Default: nginx + Description: >- + The url of a docker image that contains the application process that will + handle the traffic for this service + ContainerPort: + Type: Number + Default: 80 + Description: What port number the application inside the docker container is binding to + ContainerCpu: + Type: Number + Default: 1024 + Description: How much CPU to give the container. 1024 is 1 CPU + ContainerMemory: + Type: Number + Default: 2048 + Description: How much memory in megabytes to give the container + BUILDID: + Type: String + Default: '' + Command: + Type: String + Default: 'ls' + EntryPoint: + Type: String + Default: '/bin/sh' + WorkingDirectory: + Type: String + Default: '/efsdata/' + Role: + Type: String + Default: '' + Description: >- + (Optional) An IAM role to give the service's containers if the code within + needs to access other AWS resources like S3 buckets, DynamoDB tables, etc + EFSMountDirectory: + Type: String + Default: '/efsdata' + GithubToken: + Type: String + Default: '0' + UnityLicense: + Type: String + Default: '0' + UnityEmail: + Type: String + Default: '0' + UnityPassword: + Type: String + Default: '0' + UnitySerial: + Type: String + Default: '0' + AndroidKeystoreBase64: + Type: String + Default: '0' + AndroidKeystorePass: + Type: String + Default: '0' + AndroidKeyAliasPass: + Type: String + Default: '0' + AWSAccessKeyID: + Type: String + Default: '0' + AWSSecretAccessKey: + Type: String + Default: '0' +Mappings: + SubnetConfig: + VPC: + CIDR: 10.0.0.0/16 + PublicOne: + CIDR: 10.0.0.0/24 + PublicTwo: + CIDR: 10.0.1.0/24 +Conditions: + HasCustomRole: !Not + - !Equals + - Ref: Role + - '' +Resources: + LogGroup: + Type: 'AWS::Logs::LogGroup' + Properties: + LogGroupName: !Ref ServiceName + Metadata: + 'AWS::CloudFormation::Designer': + id: aece53ae-b82d-4267-bc16-ed964b05db27 + SubscriptionFilter: + Type: 'AWS::Logs::SubscriptionFilter' + Properties: + FilterPattern: '' + RoleArn: + 'Fn::ImportValue': !Sub '${EnvironmentName}:CloudWatchIAMRole' + LogGroupName: !Ref ServiceName + DestinationArn: + 'Fn::GetAtt': + - KinesisStream + - Arn + Metadata: + 'AWS::CloudFormation::Designer': + id: 7f809e91-9e5d-4678-98c1-c5085956c480 + DependsOn: + - LogGroup + - KinesisStream + KinesisStream: + Type: 'AWS::Kinesis::Stream' + Properties: + Name: !Ref ServiceName + ShardCount: 1 + Metadata: + 'AWS::CloudFormation::Designer': + id: c6f18447-b879-4696-8873-f981b2cedd2b + + GithubTokenSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Join [ "", [ 'GithubToken', !Ref BUILDID ] ] + SecretString: !Ref GithubToken + + UnityLicenseSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Join [ "", [ 'UnityLicense', !Ref BUILDID ] ] + SecretString: !Ref UnityLicense + + UnityEmailSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Join [ "", [ 'UnityEmail', !Ref BUILDID ] ] + SecretString: !Ref UnityEmail + + UnityPasswordSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Join [ "", [ 'UnityPassword', !Ref BUILDID ] ] + SecretString: !Ref UnityPassword + + UnitySerialSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Join [ "", [ 'UnitySerial', !Ref BUILDID ] ] + SecretString: !Ref UnitySerial + + AndroidKeystoreBase64Secret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Join [ "", [ 'AndroidKeystoreBase64', !Ref BUILDID ] ] + SecretString: !Ref AndroidKeystoreBase64 + + AndroidKeystorePassSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Join [ "", [ 'AndroidKeystorePass', !Ref BUILDID ] ] + SecretString: !Ref AndroidKeystorePass + + AndroidKeyAliasPassSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Join [ "", [ 'AndroidKeyAliasPass', !Ref BUILDID ] ] + SecretString: !Ref AndroidKeyAliasPass + AWSAccessKeyIDSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Join [ "", [ 'AWSAccessKeyID', !Ref BUILDID ] ] + SecretString: !Ref AWSAccessKeyID + AWSSecretAccessKeySecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Join [ "", [ 'AWSSecretAccessKey', !Ref BUILDID ] ] + SecretString: !Ref AWSSecretAccessKey + + TaskDefinition: + Type: 'AWS::ECS::TaskDefinition' + Properties: + Family: !Ref ServiceName + Cpu: !Ref ContainerCpu + Memory: !Ref ContainerMemory + NetworkMode: awsvpc + Volumes: + - Name: efs-data + EFSVolumeConfiguration: + FilesystemId: + 'Fn::ImportValue': !Sub '${EnvironmentName}:EfsFileStorageId' + TransitEncryption: ENABLED + RequiresCompatibilities: + - FARGATE + ExecutionRoleArn: + 'Fn::ImportValue': !Sub '${EnvironmentName}:ECSTaskExecutionRole' + TaskRoleArn: + 'Fn::If': + - HasCustomRole + - !Ref Role + - !Ref 'AWS::NoValue' + ContainerDefinitions: + - Name: !Ref ServiceName + Cpu: !Ref ContainerCpu + Memory: !Ref ContainerMemory + Image: !Ref ImageUrl + EntryPoint: + Fn::Split: + - "," + - !Ref EntryPoint + Command: + Fn::Split: + - "," + - !Ref Command + WorkingDirectory: !Ref WorkingDirectory + Environment: + - Name: ALLOW_EMPTY_PASSWORD + Value: 'yes' + MountPoints: + - SourceVolume: efs-data + ContainerPath: !Ref EFSMountDirectory + ReadOnly: false + Secrets: + - Name: 'GITHUB_TOKEN' + ValueFrom: !Ref GithubTokenSecret + - Name: 'UNITY_LICENSE' + ValueFrom: !Ref UnityLicenseSecret + - Name: 'UNITY_EMAIL' + ValueFrom: !Ref UnityEmailSecret + - Name: 'UNITY_PASSWORD' + ValueFrom: !Ref UnityPasswordSecret + - Name: 'UNITY_SERIAL' + ValueFrom: !Ref UnitySerialSecret + - Name: 'ANDROID_KEYSTORE_BASE64' + ValueFrom: !Ref AndroidKeystoreBase64Secret + - Name: 'ANDROID_KEYSTORE_PASS' + ValueFrom: !Ref AndroidKeystorePassSecret + - Name: 'AWS_ACCESS_KEY_ID' + ValueFrom: !Ref AWSAccessKeyIDSecret + - Name: 'AWS_SECRET_ACCESS_KEY' + ValueFrom: !Ref AWSSecretAccessKeySecret + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref ServiceName + awslogs-region: !Ref 'AWS::Region' + awslogs-stream-prefix: !Ref ServiceName + Metadata: + 'AWS::CloudFormation::Designer': + id: dabb0116-abe0-48a6-a8af-cf9111c879a5 + DependsOn: + - LogGroup +Metadata: + 'AWS::CloudFormation::Designer': + dabb0116-abe0-48a6-a8af-cf9111c879a5: + size: + width: 60 + height: 60 + position: + x: 270 + 'y': 90 + z: 1 + embeds: [] + dependson: + - aece53ae-b82d-4267-bc16-ed964b05db27 + c6f18447-b879-4696-8873-f981b2cedd2b: + size: + width: 60 + height: 60 + position: + x: 270 + 'y': 210 + z: 1 + embeds: [] + 7f809e91-9e5d-4678-98c1-c5085956c480: + size: + width: 60 + height: 60 + position: + x: 60 + 'y': 300 + z: 1 + embeds: [] + dependson: + - aece53ae-b82d-4267-bc16-ed964b05db27 + - c6f18447-b879-4696-8873-f981b2cedd2b + aece53ae-b82d-4267-bc16-ed964b05db27: + size: + width: 150 + height: 150 + position: + x: 60 + 'y': 90 + z: 1 + embeds: [] + 4d2da56c-3643-46b8-aaee-e46e19f95fcc: + source: + id: 7f809e91-9e5d-4678-98c1-c5085956c480 + target: + id: aece53ae-b82d-4267-bc16-ed964b05db27 + z: 11 + 14eb957b-f094-4653-93c4-77b2f851953c: + source: + id: 7f809e91-9e5d-4678-98c1-c5085956c480 + target: + id: c6f18447-b879-4696-8873-f981b2cedd2b + z: 12 + 85c57444-e5bb-4230-bc85-e545cd4558f6: + source: + id: dabb0116-abe0-48a6-a8af-cf9111c879a5 + target: + id: aece53ae-b82d-4267-bc16-ed964b05db27 + z: 13