-
Notifications
You must be signed in to change notification settings - Fork 3.9k
/
instance-drain-hook.ts
135 lines (119 loc) · 4.47 KB
/
instance-drain-hook.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import * as fs from 'fs';
import * as path from 'path';
import * as autoscaling from '@aws-cdk/aws-autoscaling';
import * as hooks from '@aws-cdk/aws-autoscaling-hooktargets';
import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import * as lambda from '@aws-cdk/aws-lambda';
import * as cdk from '@aws-cdk/core';
import { Construct } from 'constructs';
import { ICluster } from '../cluster';
// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
// eslint-disable-next-line no-duplicate-imports, import/order
import { Construct as CoreConstruct } from '@aws-cdk/core';
// Reference for the source in this package:
//
// https://github.com/aws-samples/ecs-refarch-cloudformation/blob/master/infrastructure/lifecyclehook.yaml
/**
* Properties for instance draining hook
*/
export interface InstanceDrainHookProps {
/**
* The AutoScalingGroup to install the instance draining hook for
*/
autoScalingGroup: autoscaling.IAutoScalingGroup;
/**
* The cluster on which tasks have been scheduled
*/
cluster: ICluster;
/**
* How many seconds to give tasks to drain before the instance is terminated anyway
*
* Must be between 0 and 15 minutes.
*
* @default Duration.minutes(15)
*/
drainTime?: cdk.Duration;
/**
* The InstanceDrainHook creates an SNS topic for the lifecycle hook of the ASG. If provided, then this
* key will be used to encrypt the contents of that SNS Topic.
* See [SNS Data Encryption](https://docs.aws.amazon.com/sns/latest/dg/sns-data-encryption.html) for more information.
*
* @default The SNS Topic will not be encrypted.
*/
topicEncryptionKey?: kms.IKey;
}
/**
* A hook to drain instances from ECS traffic before they're terminated
*/
export class InstanceDrainHook extends CoreConstruct {
/**
* Constructs a new instance of the InstanceDrainHook class.
*/
constructor(scope: Construct, id: string, props: InstanceDrainHookProps) {
super(scope, id);
const drainTime = props.drainTime || cdk.Duration.minutes(5);
// Invoke Lambda via SNS Topic
const fn = new lambda.Function(this, 'Function', {
code: lambda.Code.fromInline(fs.readFileSync(path.join(__dirname, 'lambda-source', 'index.py'), { encoding: 'utf-8' })),
handler: 'index.lambda_handler',
runtime: lambda.Runtime.PYTHON_3_9,
// Timeout: some extra margin for additional API calls made by the Lambda,
// up to a maximum of 15 minutes.
timeout: cdk.Duration.seconds(Math.min(drainTime.toSeconds() + 10, 900)),
environment: {
CLUSTER: props.cluster.clusterName,
},
});
// Hook everything up: ASG -> Topic, Topic -> Lambda
props.autoScalingGroup.addLifecycleHook('DrainHook', {
lifecycleTransition: autoscaling.LifecycleTransition.INSTANCE_TERMINATING,
defaultResult: autoscaling.DefaultResult.CONTINUE,
notificationTarget: new hooks.FunctionHook(fn, props.topicEncryptionKey),
heartbeatTimeout: drainTime,
});
// Describe actions cannot be restricted and restrict the CompleteLifecycleAction to the ASG arn
// https://docs.aws.amazon.com/autoscaling/ec2/userguide/control-access-using-iam.html
fn.addToRolePolicy(new iam.PolicyStatement({
actions: [
'ec2:DescribeInstances',
'ec2:DescribeInstanceAttribute',
'ec2:DescribeInstanceStatus',
'ec2:DescribeHosts',
],
resources: ['*'],
}));
// Restrict to the ASG
fn.addToRolePolicy(new iam.PolicyStatement({
actions: ['autoscaling:CompleteLifecycleAction'],
resources: [props.autoScalingGroup.autoScalingGroupArn],
}));
fn.addToRolePolicy(new iam.PolicyStatement({
actions: ['ecs:DescribeContainerInstances', 'ecs:DescribeTasks'],
resources: ['*'],
conditions: {
ArnEquals: { 'ecs:cluster': props.cluster.clusterArn },
},
}));
// Restrict to the ECS Cluster
fn.addToRolePolicy(new iam.PolicyStatement({
actions: [
'ecs:ListContainerInstances',
'ecs:SubmitContainerStateChange',
'ecs:SubmitTaskStateChange',
],
resources: [props.cluster.clusterArn],
}));
// Restrict the container-instance operations to the ECS Cluster
fn.addToRolePolicy(new iam.PolicyStatement({
actions: [
'ecs:UpdateContainerInstancesState',
'ecs:ListTasks',
],
conditions: {
ArnEquals: { 'ecs:cluster': props.cluster.clusterArn },
},
resources: ['*'],
}));
}
}