-
Notifications
You must be signed in to change notification settings - Fork 3.9k
/
target.ts
195 lines (179 loc) · 7.15 KB
/
target.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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import { ISchedule, ScheduleTargetConfig, ScheduleTargetInput } from '@aws-cdk/aws-scheduler-alpha';
import { Annotations, Duration, Names, PhysicalName, Token, Stack } from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import { CfnSchedule } from 'aws-cdk-lib/aws-scheduler';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import { md5hash } from 'aws-cdk-lib/core/lib/helpers-internal';
import { sameEnvDimension } from './util';
/**
* Base properties for a Schedule Target
*/
export interface ScheduleTargetBaseProps {
/**
* An execution role is an IAM role that EventBridge Scheduler assumes in order to interact with other AWS services on your behalf.
*
* If none provided templates target will automatically create an IAM role with all the minimum necessary
* permissions to interact with the templated target. If you wish you may specify your own IAM role, then the templated targets
* will grant minimal required permissions.
*
* Universal target automatically create an IAM role if you do not specify your own IAM role.
* However, in comparison with templated targets, for universal targets you must grant the required
* IAM permissions yourself.
*
* @default - created by target
*/
readonly role?: iam.IRole;
/**
* The SQS queue to be used as deadLetterQueue.
*
* The events not successfully delivered are automatically retried for a specified period of time,
* depending on the retry policy of the target.
* If an event is not delivered before all retry attempts are exhausted, it will be sent to the dead letter queue.
*
* @default - no dead-letter queue
*/
readonly deadLetterQueue?: sqs.IQueue;
/**
* Input passed to the target.
*
* @default - no input.
*/
readonly input?: ScheduleTargetInput;
/**
* The maximum age of a request that Scheduler sends to a target for processing.
*
* Minimum value of 60.
* Maximum value of 86400.
*
* @default Duration.hours(24)
*/
readonly maxEventAge?: Duration;
/**
* The maximum number of times to retry when the target returns an error.
*
* Minimum value of 0.
* Maximum value of 185.
*
* @default 185
*/
readonly retryAttempts?: number;
}
/**
* Base class for Schedule Targets
*/
export abstract class ScheduleTargetBase {
constructor(
private readonly baseProps: ScheduleTargetBaseProps,
protected readonly targetArn: string,
) {
}
protected abstract addTargetActionToRole(schedule: ISchedule, role: iam.IRole): void;
protected bindBaseTargetConfig(_schedule: ISchedule): ScheduleTargetConfig {
const role: iam.IRole = this.baseProps.role ?? this.singletonScheduleRole(_schedule, this.targetArn);
this.addTargetActionToRole(_schedule, role);
if (this.baseProps.deadLetterQueue) {
this.addToDeadLetterQueueResourcePolicy(_schedule, this.baseProps.deadLetterQueue);
}
return {
arn: this.targetArn,
role: role,
deadLetterConfig: this.baseProps.deadLetterQueue ? {
arn: this.baseProps.deadLetterQueue.queueArn,
} : undefined,
retryPolicy: this.renderRetryPolicy(this.baseProps.maxEventAge, this.baseProps.retryAttempts),
input: this.baseProps.input,
};
}
/**
* Create a return a Schedule Target Configuration for the given schedule
* @param schedule
* @returnn
*/
bind(schedule: ISchedule): ScheduleTargetConfig {
return this.bindBaseTargetConfig(schedule);
}
/**
* Obtain the Role for the EventBridge Scheduler event
*
* If a role already exists, it will be returned. This ensures that if multiple
* events have the same target, they will share a role.
*/
private singletonScheduleRole(schedule: ISchedule, targetArn: string): iam.IRole {
const stack = Stack.of(schedule);
const arn = Token.isUnresolved(targetArn) ? stack.resolve(targetArn).toString() : targetArn;
const hash = md5hash(arn).slice(0, 6);
const id = 'SchedulerRoleForTarget-' + hash;
const existingRole = stack.node.tryFindChild(id) as iam.Role;
const principal = new iam.PrincipalWithConditions(new iam.ServicePrincipal('scheduler.amazonaws.com'), {
StringEquals: {
'aws:SourceAccount': schedule.env.account,
},
});
if (existingRole) {
existingRole.assumeRolePolicy?.addStatements(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
principals: [principal],
actions: ['sts:AssumeRole'],
}));
return existingRole;
}
const role = new iam.Role(stack, id, {
roleName: PhysicalName.GENERATE_IF_NEEDED,
assumedBy: principal,
});
return role;
}
/**
* Allow a schedule to send events with failed invocation to an Amazon SQS queue.
* @param schedule schedule to add DLQ to
* @param queue the DLQ
*/
private addToDeadLetterQueueResourcePolicy(schedule: ISchedule, queue: sqs.IQueue) {
if (!sameEnvDimension(schedule.env.region, queue.env.region)) {
throw new Error(`Cannot assign Dead Letter Queue in region ${queue.env.region} to the schedule ${Names.nodeUniqueId(schedule.node)} in region ${schedule.env.region}. Both the queue and the schedule must be in the same region.`);
}
// Skip Resource Policy creation if the Queue is not in the same account.
// There is no way to add a target onto an imported schedule, so we can assume we will run the following code only
// in the account where the schedule is created.
if (sameEnvDimension(schedule.env.account, queue.env.account)) {
const policyStatementId = `AllowSchedule${Names.nodeUniqueId(schedule.node)}`;
queue.addToResourcePolicy(new iam.PolicyStatement({
sid: policyStatementId,
principals: [new iam.ServicePrincipal('scheduler.amazonaws.com')],
effect: iam.Effect.ALLOW,
actions: ['sqs:SendMessage'],
resources: [queue.queueArn],
}));
} else {
Annotations.of(schedule).addWarning(`Cannot add a resource policy to your dead letter queue associated with schedule ${schedule.scheduleName} because the queue is in a different account. You must add the resource policy manually to the dead letter queue in account ${queue.env.account}.`);
}
}
private renderRetryPolicy(maximumEventAge: Duration | undefined, maximumRetryAttempts: number | undefined): CfnSchedule.RetryPolicyProperty {
const maxMaxAge = Duration.days(1).toSeconds();
const minMaxAge = Duration.minutes(15).toSeconds();
let maxAge: number = maxMaxAge;
if (maximumEventAge) {
maxAge = maximumEventAge.toSeconds({ integral: true });
if (maxAge > maxMaxAge) {
throw new Error('Maximum event age is 1 day');
}
if (maxAge < minMaxAge) {
throw new Error('Minimum event age is 15 minutes');
}
};
let maxAttempts = 185;
if (typeof maximumRetryAttempts != 'undefined') {
if (maximumRetryAttempts < 0) {
throw Error('Number of retry attempts should be greater or equal than 0');
}
if (maximumRetryAttempts > 185) {
throw Error('Number of retry attempts should be less or equal than 185');
}
maxAttempts = maximumRetryAttempts;
}
return {
maximumEventAgeInSeconds: maxAge,
maximumRetryAttempts: maxAttempts,
};
}
}