import * as path from 'path' ; import * as dynamodb from '@aws-cdk/aws-dynamodb' ; import * as ecs from '@aws-cdk/aws-ecs' ; import * as events from '@aws-cdk/aws-events' ; import * as events_targets from '@aws-cdk/aws-events-targets' ; import * as iam from '@aws-cdk/aws-iam' ; import * as lambda from '@aws-cdk/aws-lambda' ; import * as lambda_es from '@aws-cdk/aws-lambda-event-sources' ; import * as route53 from '@aws-cdk/aws-route53' ; import * as sqs from '@aws-cdk/aws-sqs' ; import * as cdk from '@aws-cdk/core' ; import * as customresources from '@aws-cdk/custom-resources' ; import { Construct } from '@aws-cdk/core' ; export interface TaskRecordManagerProps { service : ecs . Ec2Service | ecs . FargateService ; dnsZone : route53 . IHostedZone ; dnsRecordName : string ; } /**
 * An event-driven serverless app to maintain a list of public ips in a Route 53
 * hosted zone.
 */ export class TaskRecordManager extends Construct { constructor ( scope , id , props ) { super ( scope , id ) ; const deadLetterQueue = new sqs . Queue ( this , 'EventsDL' , { retentionPeriod : cdk . Duration . days ( 14 ) , } ) ; const eventsQueueVisibilityTimeout = cdk . Duration . seconds ( 30 ) ; const eventsQueue = new sqs . Queue ( this , 'EventsQueue' , { deadLetterQueue : { maxReceiveCount : 500 , queue : deadLetterQueue , } , visibilityTimeout : eventsQueueVisibilityTimeout , } ) ; const recordsTable = new dynamodb . Table ( this , 'Records' , { partitionKey : { name : 'cluster_service' , type : dynamodb . AttributeType . STRING , } , billingMode : dynamodb . BillingMode . PAY_PER_REQUEST , removalPolicy : cdk . RemovalPolicy . DESTROY , } ) ; const runningEventRule = new events . Rule ( this , 'RuleRunning' , { eventPattern : { source : [ 'aws.ecs' ] , detailType : [ 'ECS Task State Change' ] , detail : { clusterArn : [ props . service . cluster . clusterArn ] , lastStatus : [ 'RUNNING' ] , desiredStatus : [ 'RUNNING' ] , } , } , targets : [ new events_targets . SqsQueue ( eventsQueue ) , ] , } ) ; const stoppedEventRule = new events . Rule ( this , 'RuleStopped' , { eventPattern : { source : [ 'aws.ecs' ] , detailType : [ 'ECS Task State Change' ] , detail : { clusterArn : [ props . service . cluster . clusterArn ] , lastStatus : [ 'STOPPED' ] , desiredStatus : [ 'STOPPED' ] , } , } , targets : [ new events_targets . SqsQueue ( eventsQueue ) , ] , } ) ; const code = lambda . Code . fromAsset ( path . join ( __dirname , 'lambda' ) , { exclude : [ '.coverage' , '*.pyc' , '.idea' , ] , } ) ; const recordFqdn = cdk . Fn . join ( '.' , [ props . dnsRecordName , props . dnsZone . zoneName ] ) ; const dnsPolicyStatement = new iam . PolicyStatement ( { effect : iam . Effect . ALLOW , actions : [ 'route53:ChangeResourceRecordSets' , 'route53:ListResourceRecordSets' , ] , resources : [ props . dnsZone . hostedZoneArn ] , } ) ; const eventHandler = new lambda . Function ( this , 'EventHandler' , { code : code , handler : 'index.queue_handler' , runtime : lambda . Runtime . PYTHON_3_8 , timeout : eventsQueueVisibilityTimeout , reservedConcurrentExecutions : 1 , environment : { HOSTED_ZONE_ID : props . dnsZone . hostedZoneId , RECORD_NAME : recordFqdn , RECORDS_TABLE : recordsTable . tableName , CLUSTER_ARN : props . service . cluster . clusterArn , SERVICE_NAME : props . service . serviceName , } , events : [ new lambda_es . SqsEventSource ( eventsQueue ) , ] , initialPolicy : [ new iam . PolicyStatement ( { effect : iam . Effect . ALLOW , actions : [ 'ec2:DescribeNetworkInterfaces' ] , resources : [ '*' ] , } ) , dnsPolicyStatement , ] , } ) ; recordsTable . grantReadWriteData ( eventHandler ) ; const cleanupResourceProviderHandler = new lambda . Function ( this , 'CleanupResourceProviderHandler' , { code : code , handler : 'index.cleanup_resource_handler' , runtime : lambda . Runtime . PYTHON_3_8 , timeout : cdk . Duration . minutes ( 5 ) , initialPolicy : [ dnsPolicyStatement , ] , } ) ; const cleanupResourceProvider = new customresources . Provider ( this , 'CleanupResourceProvider' , { onEventHandler : cleanupResourceProviderHandler , } ) ; const cleanupResource = new cdk . CustomResource ( this , 'Cleanup' , { serviceToken : cleanupResourceProvider . serviceToken , properties : { HostedZoneId : props . dnsZone . hostedZoneId , RecordName : recordFqdn , } , } ) ; const primingSdkCall = { service : 'SQS' , action : 'sendMessage' , parameters : { QueueUrl : eventsQueue . queueUrl , DelaySeconds : 10 , MessageBody : '{ "prime": true }' , MessageAttributes : { HostedZoneId : { DataType : 'String' , StringValue : props . dnsZone . hostedZoneId } , RecordName : { DataType : 'String' , StringValue : props . dnsRecordName } , } , } , physicalResourceId : customresources . PhysicalResourceId . fromResponse ( 'MessageId' ) , } ; const primingCall = new customresources . AwsCustomResource ( this , 'PrimingCall' , { onCreate : primingSdkCall , onUpdate : primingSdkCall , policy : customresources . AwsCustomResourcePolicy . fromStatements ( [ new iam . PolicyStatement ( { effect : iam . Effect . ALLOW , actions : [ 'sqs:SendMessage' ] , resources : [ eventsQueue . queueArn ] , } ) , ] ) , } ) ; primingCall . node . addDependency ( eventHandler ) ; props . service . taskDefinition . node . addDependency ( cleanupResource ) ; props . service . taskDefinition . node . addDependency ( runningEventRule ) ; props . service . taskDefinition . node . addDependency ( stoppedEventRule ) ; } }