Skip to content

Commit f5d0ab4

Browse files
authored
adds layers to function (#1835)
* adds referenceLayer to function * api.md and lint * api.md * remove referenceLayer * change set * update error to Amplify error type * update jsdocs * update jsdoc * change jsdoc * update layers * update layers * update to check for unqiue arns and adds a test * update to use set * fix api.md error * rm unrelated api.md changes * fix api.md
1 parent 3a29d43 commit f5d0ab4

File tree

6 files changed

+308
-21
lines changed

6 files changed

+308
-21
lines changed

.changeset/serious-dragons-attack.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@aws-amplify/backend-function': minor
3+
---
4+
5+
adds support to reference existing layers in defineFunction

.changeset/strong-flowers-warn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@aws-amplify/backend': minor
3+
---
4+
5+
adds support to reference existing layers in defineFunction

packages/backend-function/API.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type FunctionProps = {
3131
environment?: Record<string, string | BackendSecret>;
3232
runtime?: NodeVersion;
3333
schedule?: FunctionSchedule | FunctionSchedule[];
34+
layers?: Record<string, string>;
3435
};
3536

3637
// @public (undocumented)

packages/backend-function/src/factory.ts

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
import {
2+
FunctionOutput,
3+
functionOutputKey,
4+
} from '@aws-amplify/backend-output-schemas';
5+
import { AttributionMetadataStorage } from '@aws-amplify/backend-output-storage';
6+
import {
7+
AmplifyUserError,
8+
CallerDirectoryExtractor,
9+
TagName,
10+
} from '@aws-amplify/platform-core';
111
import {
212
BackendOutputStorageStrategy,
313
BackendSecret,
@@ -13,31 +23,27 @@ import {
1323
SsmEnvironmentEntry,
1424
StackProvider,
1525
} from '@aws-amplify/plugin-types';
16-
import { Construct } from 'constructs';
17-
import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs';
18-
import * as path from 'path';
1926
import { Duration, Stack, Tags } from 'aws-cdk-lib';
20-
import { CfnFunction, Runtime } from 'aws-cdk-lib/aws-lambda';
21-
import { createRequire } from 'module';
22-
import { FunctionEnvironmentTranslator } from './function_env_translator.js';
27+
import { Rule } from 'aws-cdk-lib/aws-events';
28+
import * as targets from 'aws-cdk-lib/aws-events-targets';
2329
import { Policy } from 'aws-cdk-lib/aws-iam';
30+
import {
31+
CfnFunction,
32+
ILayerVersion,
33+
LayerVersion,
34+
Runtime,
35+
} from 'aws-cdk-lib/aws-lambda';
36+
import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs';
37+
import { Construct } from 'constructs';
2438
import { readFileSync } from 'fs';
39+
import { createRequire } from 'module';
40+
import { fileURLToPath } from 'node:url';
2541
import { EOL } from 'os';
26-
import {
27-
FunctionOutput,
28-
functionOutputKey,
29-
} from '@aws-amplify/backend-output-schemas';
42+
import * as path from 'path';
43+
import { FunctionEnvironmentTranslator } from './function_env_translator.js';
3044
import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.js';
31-
import { AttributionMetadataStorage } from '@aws-amplify/backend-output-storage';
32-
import { fileURLToPath } from 'node:url';
33-
import {
34-
AmplifyUserError,
35-
CallerDirectoryExtractor,
36-
TagName,
37-
} from '@aws-amplify/platform-core';
45+
import { FunctionLayerArnParser } from './layer_parser.js';
3846
import { convertFunctionSchedulesToRuleSchedules } from './schedule_parser.js';
39-
import * as targets from 'aws-cdk-lib/aws-events-targets';
40-
import { Rule } from 'aws-cdk-lib/aws-events';
4147

4248
const functionStackType = 'function-Lambda';
4349

@@ -126,6 +132,19 @@ export type FunctionProps = {
126132
* schedule: "0 9 ? * 2 *" // every Monday at 9am
127133
*/
128134
schedule?: FunctionSchedule | FunctionSchedule[];
135+
136+
/**
137+
* Attach Lambda layers to a function
138+
* - A Lambda layer is represented by an object of key/value pair where the key is the module name that is exported from your layer and the value is the ARN of the layer. The key (module name) is used to externalize the module dependency so it doesn't get bundled with your lambda function
139+
* - Maximum of 5 layers can be attached to a function and must be in the same region as the function.
140+
* @example
141+
* layers: {
142+
* "@aws-lambda-powertools/logger": "arn:aws:lambda:<current-region>:094274105915:layer:AWSLambdaPowertoolsTypeScriptV2:11"
143+
* },
144+
* @see [Amplify documentation for Lambda layers](https://docs.amplify.aws/react/build-a-backend/functions/add-lambda-layers)
145+
* @see [AWS documentation for Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html)
146+
*/
147+
layers?: Record<string, string>;
129148
};
130149

131150
/**
@@ -163,6 +182,8 @@ class FunctionFactory implements ConstructFactory<AmplifyFunction> {
163182
): HydratedFunctionProps => {
164183
const name = this.resolveName();
165184
resourceNameValidator?.validate(name);
185+
const parser = new FunctionLayerArnParser();
186+
const layers = parser.parseLayers(this.props.layers ?? {}, name);
166187
return {
167188
name,
168189
entry: this.resolveEntry(),
@@ -171,6 +192,7 @@ class FunctionFactory implements ConstructFactory<AmplifyFunction> {
171192
environment: this.props.environment ?? {},
172193
runtime: this.resolveRuntime(),
173194
schedule: this.resolveSchedule(),
195+
layers,
174196
};
175197
};
176198

@@ -292,10 +314,19 @@ class FunctionGenerator implements ConstructContainerEntryGenerator {
292314
scope,
293315
backendSecretResolver,
294316
}: GenerateContainerEntryProps) => {
317+
// resolve layers to LayerVersion objects for the NodejsFunction constructor using the scope.
318+
const resolvedLayers = Object.entries(this.props.layers).map(([key, arn]) =>
319+
LayerVersion.fromLayerVersionArn(
320+
scope,
321+
`${this.props.name}-${key}-layer`,
322+
arn
323+
)
324+
);
325+
295326
return new AmplifyFunction(
296327
scope,
297328
this.props.name,
298-
this.props,
329+
{ ...this.props, resolvedLayers },
299330
backendSecretResolver,
300331
this.outputStorageStrategy
301332
);
@@ -315,7 +346,7 @@ class AmplifyFunction
315346
constructor(
316347
scope: Construct,
317348
id: string,
318-
props: HydratedFunctionProps,
349+
props: HydratedFunctionProps & { resolvedLayers: ILayerVersion[] },
319350
backendSecretResolver: BackendSecretResolver,
320351
outputStorageStrategy: BackendOutputStorageStrategy<FunctionOutput>
321352
) {
@@ -365,6 +396,7 @@ class AmplifyFunction
365396
timeout: Duration.seconds(props.timeoutSeconds),
366397
memorySize: props.memoryMB,
367398
runtime: nodeVersionMap[props.runtime],
399+
layers: props.resolvedLayers,
368400
bundling: {
369401
format: OutputFormat.ESM,
370402
banner: bannerCode,
@@ -375,6 +407,7 @@ class AmplifyFunction
375407
},
376408
minify: true,
377409
sourceMap: true,
410+
externalModules: Object.keys(props.layers),
378411
},
379412
});
380413
} catch (error) {
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { StackMetadataBackendOutputStorageStrategy } from '@aws-amplify/backend-output-storage';
2+
import {
3+
ConstructContainerStub,
4+
ResourceNameValidatorStub,
5+
StackResolverStub,
6+
} from '@aws-amplify/backend-platform-test-stubs';
7+
import { AmplifyUserError } from '@aws-amplify/platform-core';
8+
import {
9+
ConstructFactoryGetInstanceProps,
10+
ResourceNameValidator,
11+
} from '@aws-amplify/plugin-types';
12+
import { App, Stack } from 'aws-cdk-lib';
13+
import { Template } from 'aws-cdk-lib/assertions';
14+
import assert from 'node:assert';
15+
import { beforeEach, describe, it } from 'node:test';
16+
import { defineFunction } from './factory.js';
17+
18+
const createStackAndSetContext = (): Stack => {
19+
const app = new App();
20+
app.node.setContext('amplify-backend-name', 'testEnvName');
21+
app.node.setContext('amplify-backend-namespace', 'testBackendId');
22+
app.node.setContext('amplify-backend-type', 'branch');
23+
const stack = new Stack(app);
24+
return stack;
25+
};
26+
27+
void describe('AmplifyFunctionFactory - Layers', () => {
28+
let rootStack: Stack;
29+
let getInstanceProps: ConstructFactoryGetInstanceProps;
30+
let resourceNameValidator: ResourceNameValidator;
31+
32+
beforeEach(() => {
33+
rootStack = createStackAndSetContext();
34+
35+
const constructContainer = new ConstructContainerStub(
36+
new StackResolverStub(rootStack)
37+
);
38+
39+
const outputStorageStrategy = new StackMetadataBackendOutputStorageStrategy(
40+
rootStack
41+
);
42+
43+
resourceNameValidator = new ResourceNameValidatorStub();
44+
45+
getInstanceProps = {
46+
constructContainer,
47+
outputStorageStrategy,
48+
resourceNameValidator,
49+
};
50+
});
51+
52+
void it('sets a valid layer', () => {
53+
const layerArn = 'arn:aws:lambda:us-east-1:123456789012:layer:my-layer:1';
54+
const functionFactory = defineFunction({
55+
entry: './test-assets/default-lambda/handler.ts',
56+
name: 'lambdaWithLayer',
57+
layers: {
58+
myLayer: layerArn,
59+
},
60+
});
61+
const lambda = functionFactory.getInstance(getInstanceProps);
62+
const template = Template.fromStack(Stack.of(lambda.resources.lambda));
63+
64+
template.resourceCountIs('AWS::Lambda::Function', 1);
65+
template.hasResourceProperties('AWS::Lambda::Function', {
66+
Handler: 'index.handler',
67+
Layers: [layerArn],
68+
});
69+
});
70+
71+
void it('sets multiple valid layers', () => {
72+
const layerArns = [
73+
'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-1:1',
74+
'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-2:1',
75+
];
76+
const functionFactory = defineFunction({
77+
entry: './test-assets/default-lambda/handler.ts',
78+
name: 'lambdaWithMultipleLayers',
79+
layers: {
80+
myLayer1: layerArns[0],
81+
myLayer2: layerArns[1],
82+
},
83+
});
84+
const lambda = functionFactory.getInstance(getInstanceProps);
85+
const template = Template.fromStack(Stack.of(lambda.resources.lambda));
86+
87+
template.resourceCountIs('AWS::Lambda::Function', 1);
88+
template.hasResourceProperties('AWS::Lambda::Function', {
89+
Handler: 'index.handler',
90+
Layers: layerArns,
91+
});
92+
});
93+
94+
void it('throws an error for an invalid layer ARN', () => {
95+
const invalidLayerArn = 'invalid:arn';
96+
const functionFactory = defineFunction({
97+
entry: './test-assets/default-lambda/handler.ts',
98+
name: 'lambdaWithInvalidLayer',
99+
layers: {
100+
invalidLayer: invalidLayerArn,
101+
},
102+
});
103+
assert.throws(
104+
() => functionFactory.getInstance(getInstanceProps),
105+
(error: AmplifyUserError) => {
106+
assert.strictEqual(
107+
error.message,
108+
`Invalid ARN format for layer: ${invalidLayerArn}`
109+
);
110+
assert.ok(error.resolution);
111+
return true;
112+
}
113+
);
114+
});
115+
116+
void it('throws an error for exceeding the maximum number of layers', () => {
117+
const layerArns = [
118+
'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-1:1',
119+
'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-2:1',
120+
'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-3:1',
121+
'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-4:1',
122+
'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-5:1',
123+
'arn:aws:lambda:us-east-1:123456789012:layer:my-layer-6:1',
124+
];
125+
const layers: Record<string, string> = layerArns.reduce(
126+
(acc, arn, index) => {
127+
acc[`layer${index + 1}`] = arn;
128+
return acc;
129+
},
130+
{} as Record<string, string>
131+
);
132+
133+
const functionFactory = defineFunction({
134+
entry: './test-assets/default-lambda/handler.ts',
135+
name: 'lambdaWithTooManyLayers',
136+
layers,
137+
});
138+
139+
assert.throws(
140+
() => functionFactory.getInstance(getInstanceProps),
141+
(error: AmplifyUserError) => {
142+
assert.strictEqual(
143+
error.message,
144+
`A maximum of 5 unique layers can be attached to a function.`
145+
);
146+
assert.ok(error.resolution);
147+
return true;
148+
}
149+
);
150+
});
151+
152+
void it('checks if only unique Arns are being used', () => {
153+
const duplicateArn =
154+
'arn:aws:lambda:us-east-1:123456789012:layer:my-layer:1';
155+
const functionFactory = defineFunction({
156+
entry: './test-assets/default-lambda/handler.ts',
157+
name: 'lambdaWithDuplicateLayers',
158+
layers: {
159+
layer1: duplicateArn,
160+
layer2: duplicateArn,
161+
layer3: duplicateArn,
162+
layer4: duplicateArn,
163+
layer5: duplicateArn,
164+
layer6: duplicateArn,
165+
},
166+
});
167+
168+
const lambda = functionFactory.getInstance(getInstanceProps);
169+
const template = Template.fromStack(Stack.of(lambda.resources.lambda));
170+
171+
template.resourceCountIs('AWS::Lambda::Function', 1);
172+
template.hasResourceProperties('AWS::Lambda::Function', {
173+
Handler: 'index.handler',
174+
Layers: [duplicateArn],
175+
});
176+
});
177+
});

0 commit comments

Comments
 (0)