-
Notifications
You must be signed in to change notification settings - Fork 3.9k
/
provider.ts
266 lines (232 loc) · 9.06 KB
/
provider.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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
import * as path from 'path';
import { Construct } from 'constructs';
import * as consts from './runtime/consts';
import { calculateRetryPolicy } from './util';
import { LogOptions, WaiterStateMachine } from './waiter-state-machine';
import { CustomResourceProviderConfig, ICustomResourceProvider } from '../../../aws-cloudformation';
import * as ec2 from '../../../aws-ec2';
import * as iam from '../../../aws-iam';
import * as kms from '../../../aws-kms';
import * as lambda from '../../../aws-lambda';
import * as logs from '../../../aws-logs';
import { Duration } from '../../../core';
const RUNTIME_HANDLER_PATH = path.join(__dirname, 'runtime');
const FRAMEWORK_HANDLER_TIMEOUT = Duration.minutes(15); // keep it simple for now
/**
* Initialization properties for the `Provider` construct.
*/
export interface ProviderProps {
/**
* The AWS Lambda function to invoke for all resource lifecycle operations
* (CREATE/UPDATE/DELETE).
*
* This function is responsible to begin the requested resource operation
* (CREATE/UPDATE/DELETE) and return any additional properties to add to the
* event, which will later be passed to `isComplete`. The `PhysicalResourceId`
* property must be included in the response.
*/
readonly onEventHandler: lambda.IFunction;
/**
* The AWS Lambda function to invoke in order to determine if the operation is
* complete.
*
* This function will be called immediately after `onEvent` and then
* periodically based on the configured query interval as long as it returns
* `false`. If the function still returns `false` and the alloted timeout has
* passed, the operation will fail.
*
* @default - provider is synchronous. This means that the `onEvent` handler
* is expected to finish all lifecycle operations within the initial invocation.
*/
readonly isCompleteHandler?: lambda.IFunction;
/**
* Time between calls to the `isComplete` handler which determines if the
* resource has been stabilized.
*
* The first `isComplete` will be called immediately after `handler` and then
* every `queryInterval` seconds, and until `timeout` has been reached or until
* `isComplete` returns `true`.
*
* @default Duration.seconds(5)
*/
readonly queryInterval?: Duration;
/**
* Total timeout for the entire operation.
*
* The maximum timeout is 1 hour (yes, it can exceed the AWS Lambda 15 minutes)
*
* @default Duration.minutes(30)
*/
readonly totalTimeout?: Duration;
/**
* The number of days framework log events are kept in CloudWatch Logs. When
* updating this property, unsetting it doesn't remove the log retention policy.
* To remove the retention policy, set the value to `INFINITE`.
*
* @default logs.RetentionDays.INFINITE
*/
readonly logRetention?: logs.RetentionDays;
/**
* The vpc to provision the lambda functions in.
*
* @default - functions are not provisioned inside a vpc.
*/
readonly vpc?: ec2.IVpc;
/**
* Which subnets from the VPC to place the lambda functions in.
*
* Only used if 'vpc' is supplied. Note: internet access for Lambdas
* requires a NAT gateway, so picking Public subnets is not allowed.
*
* @default - the Vpc default strategy if not specified
*/
readonly vpcSubnets?: ec2.SubnetSelection;
/**
* Security groups to attach to the provider functions.
*
* Only used if 'vpc' is supplied
*
* @default - If `vpc` is not supplied, no security groups are attached. Otherwise, a dedicated security
* group is created for each function.
*/
readonly securityGroups?: ec2.ISecurityGroup[];
/**
* AWS Lambda execution role.
*
* The role that will be assumed by the AWS Lambda.
* Must be assumable by the 'lambda.amazonaws.com' service principal.
*
* @default - A default role will be created.
*/
readonly role?: iam.IRole;
/**
* Provider Lambda name.
*
* The provider lambda function name.
*
* @default - CloudFormation default name from unique physical ID
*/
readonly providerFunctionName?: string;
/**
* AWS KMS key used to encrypt provider lambda's environment variables.
*
* @default - AWS Lambda creates and uses an AWS managed customer master key (CMK)
*/
readonly providerFunctionEnvEncryption?: kms.IKey;
/**
* Defines what execution history events of the waiter state machine are logged and where they are logged.
*
* @default - A default log group will be created if logging for the waiter state machine is enabled.
*/
readonly waiterStateMachineLogOptions?: LogOptions;
/**
* Whether logging for the waiter state machine is disabled.
*
* @default - false
*/
readonly disableWaiterStateMachineLogging?: boolean;
}
/**
* Defines an AWS CloudFormation custom resource provider.
*/
export class Provider extends Construct implements ICustomResourceProvider {
/**
* The user-defined AWS Lambda function which is invoked for all resource
* lifecycle operations (CREATE/UPDATE/DELETE).
*/
public readonly onEventHandler: lambda.IFunction;
/**
* The user-defined AWS Lambda function which is invoked asynchronously in
* order to determine if the operation is complete.
*/
public readonly isCompleteHandler?: lambda.IFunction;
/**
* The service token to use in order to define custom resources that are
* backed by this provider.
*/
public readonly serviceToken: string;
private readonly entrypoint: lambda.Function;
private readonly logRetention?: logs.RetentionDays;
private readonly vpc?: ec2.IVpc;
private readonly vpcSubnets?: ec2.SubnetSelection;
private readonly securityGroups?: ec2.ISecurityGroup[];
private readonly role?: iam.IRole;
private readonly providerFunctionEnvEncryption?: kms.IKey;
constructor(scope: Construct, id: string, props: ProviderProps) {
super(scope, id);
if (!props.isCompleteHandler) {
if (
props.queryInterval
|| props.totalTimeout
|| props.waiterStateMachineLogOptions
|| props.disableWaiterStateMachineLogging !== undefined
) {
throw new Error('"queryInterval", "totalTimeout", "waiterStateMachineLogOptions", and "disableWaiterStateMachineLogging" '
+ 'can only be configured if "isCompleteHandler" is specified. '
+ 'Otherwise, they have no meaning');
}
}
this.onEventHandler = props.onEventHandler;
this.isCompleteHandler = props.isCompleteHandler;
this.logRetention = props.logRetention;
this.vpc = props.vpc;
this.vpcSubnets = props.vpcSubnets;
this.securityGroups = props.securityGroups;
this.role = props.role;
this.providerFunctionEnvEncryption = props.providerFunctionEnvEncryption;
const onEventFunction = this.createFunction(consts.FRAMEWORK_ON_EVENT_HANDLER_NAME, props.providerFunctionName);
if (this.isCompleteHandler) {
const isCompleteFunction = this.createFunction(consts.FRAMEWORK_IS_COMPLETE_HANDLER_NAME);
const timeoutFunction = this.createFunction(consts.FRAMEWORK_ON_TIMEOUT_HANDLER_NAME);
const retry = calculateRetryPolicy(props);
const waiterStateMachine = new WaiterStateMachine(this, 'waiter-state-machine', {
isCompleteHandler: isCompleteFunction,
timeoutHandler: timeoutFunction,
backoffRate: retry.backoffRate,
interval: retry.interval,
maxAttempts: retry.maxAttempts,
logOptions: props.waiterStateMachineLogOptions,
disableLogging: props.disableWaiterStateMachineLogging,
});
// the on-event entrypoint is going to start the execution of the waiter
onEventFunction.addEnvironment(consts.WAITER_STATE_MACHINE_ARN_ENV, waiterStateMachine.stateMachineArn);
waiterStateMachine.grantStartExecution(onEventFunction);
}
this.entrypoint = onEventFunction;
this.serviceToken = this.entrypoint.functionArn;
}
/**
* Called by `CustomResource` which uses this provider.
* @deprecated use `provider.serviceToken` instead
*/
public bind(_scope: Construct): CustomResourceProviderConfig {
return {
serviceToken: this.entrypoint.functionArn,
};
}
private createFunction(entrypoint: string, name?: string) {
const fn = new lambda.Function(this, `framework-${entrypoint}`, {
code: lambda.Code.fromAsset(RUNTIME_HANDLER_PATH, {
exclude: ['*.ts'],
}),
description: `AWS CDK resource provider framework - ${entrypoint} (${this.node.path})`.slice(0, 256),
runtime: lambda.Runtime.NODEJS_18_X,
handler: `framework.${entrypoint}`,
timeout: FRAMEWORK_HANDLER_TIMEOUT,
logRetention: this.logRetention,
vpc: this.vpc,
vpcSubnets: this.vpcSubnets,
securityGroups: this.securityGroups,
role: this.role,
functionName: name,
environmentEncryption: this.providerFunctionEnvEncryption,
});
fn.addEnvironment(consts.USER_ON_EVENT_FUNCTION_ARN_ENV, this.onEventHandler.functionArn);
this.onEventHandler.grantInvoke(fn);
if (this.isCompleteHandler) {
fn.addEnvironment(consts.USER_IS_COMPLETE_FUNCTION_ARN_ENV, this.isCompleteHandler.functionArn);
this.isCompleteHandler.grantInvoke(fn);
}
return fn;
}
}