in packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/assign-public-ip/task-record-manager.ts [29:210]
constructor(scope: Construct, id: string, props: TaskRecordManagerProps) {
super(scope, id);
// Poison pills go here.
const deadLetterQueue = new sqs.Queue(this, 'EventsDL', {
retentionPeriod: cdk.Duration.days(14),
});
// Time limit for processing queue items - we set the lambda time limit to
// this value as well.
const eventsQueueVisibilityTimeout = cdk.Duration.seconds(30);
// This queue lets us batch together ecs task state events. This is useful
// for when when we would be otherwise bombarded by them.
const eventsQueue = new sqs.Queue(this, 'EventsQueue', {
deadLetterQueue: {
maxReceiveCount: 500,
queue: deadLetterQueue,
},
visibilityTimeout: eventsQueueVisibilityTimeout,
});
// Storage for task and record set information.
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,
});
// Put the cluster's task state changes events into the queue.
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),
],
});
// Shared codebase for the lambdas.
const code = lambda.Code.fromAsset(path.join(__dirname, 'lambda'), {
exclude: [
'.coverage',
'*.pyc',
'.idea',
],
});
// Fully qualified domain name of the record
const recordFqdn = cdk.Fn.join('.', [props.dnsRecordName, props.dnsZone.zoneName]);
// Allow access to manage a zone's records.
const dnsPolicyStatement = new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'route53:ChangeResourceRecordSets',
'route53:ListResourceRecordSets',
],
resources: [props.dnsZone.hostedZoneArn],
});
// This function consumes events from the event queue and does the work of
// querying task IP addresses and creating, updating record sets. When there
// are zero tasks, it deletes the record set.
const eventHandler = new lambda.Function(this, 'EventHandler', {
code: code,
handler: 'index.queue_handler',
runtime: lambda.Runtime.PYTHON_3_8,
timeout: eventsQueueVisibilityTimeout,
// Single-concurrency to prevent a race to set the RecordSet
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: [
// Look up task IPs
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['ec2:DescribeNetworkInterfaces'],
resources: ['*'],
}),
dnsPolicyStatement,
],
});
recordsTable.grantReadWriteData(eventHandler);
// The lambda for a custom resource provider that deletes dangling record
// sets when the stack is deleted.
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,
},
});
// Prime the event queue with a message so that changes to dns config are
// quickly applied.
const primingSdkCall: customresources.AwsSdkCall = {
service: 'SQS',
action: 'sendMessage',
parameters: {
QueueUrl: eventsQueue.queueUrl,
DelaySeconds: 10,
MessageBody: '{ "prime": true }',
// Add the hosted zone id and record name so that priming occurs with
// dns config updates.
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],
}),
]),
});
// Send the priming call after the handler is created/updated.
primingCall.node.addDependency(eventHandler);
// Ensure that the cleanup resource is deleted last (so it can clean up)
props.service.taskDefinition.node.addDependency(cleanupResource);
// Ensure that the event rules are created first so we can catch the first
// state transitions.
props.service.taskDefinition.node.addDependency(runningEventRule);
props.service.taskDefinition.node.addDependency(stoppedEventRule);
}