-
Notifications
You must be signed in to change notification settings - Fork 4k
/
Copy pathedge-function.ts
260 lines (230 loc) · 9.88 KB
/
edge-function.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
import * as path from 'path';
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam';
import * as lambda from '@aws-cdk/aws-lambda';
import * as ssm from '@aws-cdk/aws-ssm';
import {
CfnResource,
CustomResource, CustomResourceProvider, CustomResourceProviderRuntime,
Lazy, Resource, Stack, Stage, Token,
} from '@aws-cdk/core';
import { Construct, Node } from 'constructs';
/**
* Properties for creating a Lambda@Edge function
*/
export interface EdgeFunctionProps extends lambda.FunctionProps {
/**
* The stack ID of Lambda@Edge function.
*
* @default - `edge-lambda-stack-${region}`
*/
readonly stackId?: string;
}
/**
* A Lambda@Edge function.
*
* Convenience resource for requesting a Lambda function in the 'us-east-1' region for use with Lambda@Edge.
* Implements several restrictions enforced by Lambda@Edge.
*
* Note that this construct requires that the 'us-east-1' region has been bootstrapped.
* See https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html or 'cdk bootstrap --help' for options.
*
* @resource AWS::Lambda::Function
*/
export class EdgeFunction extends Resource implements lambda.IVersion {
private static readonly EDGE_REGION: string = 'us-east-1';
public readonly edgeArn: string;
public readonly functionName: string;
public readonly functionArn: string;
public readonly grantPrincipal: iam.IPrincipal;
public readonly isBoundToVpc = false;
public readonly permissionsNode: Node;
public readonly role?: iam.IRole;
public readonly version: string;
public readonly architecture: lambda.Architecture;
public readonly resourceArnsForGrantInvoke: string[];
private readonly _edgeFunction: lambda.Function;
constructor(scope: Construct, id: string, props: EdgeFunctionProps) {
super(scope, id);
// Create a simple Function if we're already in us-east-1; otherwise create a cross-region stack.
const regionIsUsEast1 = !Token.isUnresolved(this.env.region) && this.env.region === 'us-east-1';
const { edgeFunction, edgeArn } = regionIsUsEast1
? this.createInRegionFunction(props)
: this.createCrossRegionFunction(id, props);
this.edgeArn = edgeArn;
this.functionArn = edgeArn;
this._edgeFunction = edgeFunction;
this.functionName = this._edgeFunction.functionName;
this.grantPrincipal = this._edgeFunction.role!;
this.permissionsNode = this._edgeFunction.permissionsNode;
this.version = lambda.extractQualifierFromArn(this.functionArn);
this.architecture = this._edgeFunction.architecture;
this.resourceArnsForGrantInvoke = this._edgeFunction.resourceArnsForGrantInvoke;
this.node.defaultChild = this._edgeFunction;
}
public get lambda(): lambda.IFunction {
return this._edgeFunction;
}
/**
* Convenience method to make `EdgeFunction` conform to the same interface as `Function`.
*/
public get currentVersion(): lambda.IVersion {
return this;
}
public addAlias(aliasName: string, options: lambda.AliasOptions = {}): lambda.Alias {
return new lambda.Alias(this._edgeFunction, `Alias${aliasName}`, {
aliasName,
version: this._edgeFunction.currentVersion,
...options,
});
}
/**
* Not supported. Connections are only applicable to VPC-enabled functions.
*/
public get connections(): ec2.Connections {
throw new Error('Lambda@Edge does not support connections');
}
public get latestVersion(): lambda.IVersion {
throw new Error('$LATEST function version cannot be used for Lambda@Edge');
}
public addEventSourceMapping(id: string, options: lambda.EventSourceMappingOptions): lambda.EventSourceMapping {
return this.lambda.addEventSourceMapping(id, options);
}
public addPermission(id: string, permission: lambda.Permission): void {
return this.lambda.addPermission(id, permission);
}
public addToRolePolicy(statement: iam.PolicyStatement): void {
return this.lambda.addToRolePolicy(statement);
}
public grantInvoke(identity: iam.IGrantable): iam.Grant {
return this.lambda.grantInvoke(identity);
}
public grantInvokeUrl(identity: iam.IGrantable): iam.Grant {
return this.lambda.grantInvokeUrl(identity);
}
public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric {
return this.lambda.metric(metricName, { ...props, region: EdgeFunction.EDGE_REGION });
}
public metricDuration(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
return this.lambda.metricDuration({ ...props, region: EdgeFunction.EDGE_REGION });
}
public metricErrors(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
return this.lambda.metricErrors({ ...props, region: EdgeFunction.EDGE_REGION });
}
public metricInvocations(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
return this.lambda.metricInvocations({ ...props, region: EdgeFunction.EDGE_REGION });
}
public metricThrottles(props?: cloudwatch.MetricOptions): cloudwatch.Metric {
return this.lambda.metricThrottles({ ...props, region: EdgeFunction.EDGE_REGION });
}
/** Adds an event source to this function. */
public addEventSource(source: lambda.IEventSource): void {
return this.lambda.addEventSource(source);
}
public configureAsyncInvoke(options: lambda.EventInvokeConfigOptions): void {
return this.lambda.configureAsyncInvoke(options);
}
public addFunctionUrl(options?: lambda.FunctionUrlOptions): lambda.FunctionUrl {
return this.lambda.addFunctionUrl(options);
}
/** Create a function in-region */
private createInRegionFunction(props: lambda.FunctionProps): FunctionConfig {
const edgeFunction = new lambda.Function(this, 'Fn', props);
addEdgeLambdaToRoleTrustStatement(edgeFunction.role!);
return { edgeFunction, edgeArn: edgeFunction.currentVersion.edgeArn };
}
/** Create a support stack and function in us-east-1, and a SSM reader in-region */
private createCrossRegionFunction(id: string, props: EdgeFunctionProps): FunctionConfig {
const parameterNamePrefix = 'cdk/EdgeFunctionArn';
if (Token.isUnresolved(this.env.region)) {
throw new Error('stacks which use EdgeFunctions must have an explicitly set region');
}
// SSM parameter names must only contain letters, numbers, ., _, -, or /.
const sanitizedPath = this.node.path.replace(/[^\/\w.-]/g, '_');
const parameterName = `/${parameterNamePrefix}/${this.env.region}/${sanitizedPath}`;
const functionStack = this.edgeStack(props.stackId);
const edgeFunction = new lambda.Function(functionStack, id, props);
addEdgeLambdaToRoleTrustStatement(edgeFunction.role!);
// Store the current version's ARN to be retrieved by the cross region reader below.
const version = edgeFunction.currentVersion;
new ssm.StringParameter(edgeFunction, 'Parameter', {
parameterName,
stringValue: version.edgeArn,
});
const edgeArn = this.createCrossRegionArnReader(parameterNamePrefix, parameterName, version);
return { edgeFunction, edgeArn };
}
private createCrossRegionArnReader(parameterNamePrefix: string, parameterName: string, version: lambda.Version): string {
// Prefix of the parameter ARN that applies to all EdgeFunctions.
// This is necessary because the `CustomResourceProvider` is a singleton, and the `policyStatement`
// must work for multiple EdgeFunctions.
const parameterArnPrefix = this.stack.formatArn({
service: 'ssm',
region: EdgeFunction.EDGE_REGION,
resource: 'parameter',
resourceName: parameterNamePrefix + '/*',
});
const resourceType = 'Custom::CrossRegionStringParameterReader';
const serviceToken = CustomResourceProvider.getOrCreate(this, resourceType, {
codeDirectory: path.join(__dirname, 'edge-function'),
runtime: CustomResourceProviderRuntime.NODEJS_14_X,
policyStatements: [{
Effect: 'Allow',
Resource: parameterArnPrefix,
Action: ['ssm:GetParameter'],
}],
});
const resource = new CustomResource(this, 'ArnReader', {
resourceType: resourceType,
serviceToken,
properties: {
Region: EdgeFunction.EDGE_REGION,
ParameterName: parameterName,
// This is used to determine when the function has changed, to refresh the ARN from the custom resource.
//
// Use the logical id of the function version. Whenever a function version changes, the logical id must be
// changed for it to take effect - a good candidate for RefreshToken.
RefreshToken: Lazy.uncachedString({
produce: () => {
const cfn = version.node.defaultChild as CfnResource;
return this.stack.resolve(cfn.logicalId);
},
}),
},
});
return resource.getAttString('FunctionArn');
}
private edgeStack(stackId?: string): Stack {
const stage = Stage.of(this);
if (!stage) {
throw new Error('stacks which use EdgeFunctions must be part of a CDK app or stage');
}
const edgeStackId = stackId ?? `edge-lambda-stack-${this.stack.node.addr}`;
let edgeStack = stage.node.tryFindChild(edgeStackId) as Stack;
if (!edgeStack) {
edgeStack = new Stack(stage, edgeStackId, {
env: {
region: EdgeFunction.EDGE_REGION,
account: Stack.of(this).account,
},
});
}
this.stack.addDependency(edgeStack);
return edgeStack;
}
}
/** Result of creating an in-region or cross-region function */
interface FunctionConfig {
readonly edgeFunction: lambda.Function;
readonly edgeArn: string;
}
function addEdgeLambdaToRoleTrustStatement(role: iam.IRole) {
if (role instanceof iam.Role && role.assumeRolePolicy) {
const statement = new iam.PolicyStatement();
const edgeLambdaServicePrincipal = new iam.ServicePrincipal('edgelambda.amazonaws.com');
statement.addPrincipals(edgeLambdaServicePrincipal);
statement.addActions(edgeLambdaServicePrincipal.assumeRoleAction);
role.assumeRolePolicy.addStatements(statement);
}
}