Skip to content

Commit ccde77a

Browse files
authored
refactor lambda ssm param resolution to use cdk.Lazy, refresh ssm params on schedule (#982)
1 parent 1814f1a commit ccde77a

File tree

18 files changed

+452
-135
lines changed

18 files changed

+452
-135
lines changed

.changeset/kind-keys-travel.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+
Refactor secret fetcher and support node 16 ssm shim

package-lock.json

Lines changed: 8 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend-function/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@aws-amplify/backend-platform-test-stubs": "^0.3.2",
2727
"@aws-amplify/platform-core": "^0.4.3",
2828
"@aws-sdk/client-ssm": "^3.465.0",
29+
"aws-sdk": "^2.1550.0",
2930
"uuid": "^9.0.1"
3031
},
3132
"peerDependencies": {

packages/backend-function/src/factory.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ void describe('AmplifyFunctionFactory', () => {
233233
});
234234
});
235235

236-
void it('defaults to oldest runtime', () => {
236+
void it('defaults to oldest LTS runtime', () => {
237237
const lambda = defineFunction({
238238
entry: './test-assets/default-lambda/handler.ts',
239239
}).getInstance(getInstanceProps);

packages/backend-function/src/factory.ts

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import * as path from 'path';
1313
import { getCallerDirectory } from './get_caller_directory.js';
1414
import { Duration } from 'aws-cdk-lib';
1515
import { Runtime } from 'aws-cdk-lib/aws-lambda';
16-
import fs from 'fs';
1716
import { createRequire } from 'module';
1817
import { FunctionEnvironmentTranslator } from './function_env_translator.js';
1918

@@ -227,42 +226,36 @@ class AmplifyFunction
227226
backendSecretResolver: BackendSecretResolver
228227
) {
229228
super(scope, id);
230-
const functionEnvironmentTranslator = new FunctionEnvironmentTranslator(
231-
scope,
232-
props['environment'],
233-
backendSecretResolver
234-
);
235-
const environmentRecord =
236-
functionEnvironmentTranslator.getEnvironmentRecord();
237-
const secretPolicyStatement =
238-
functionEnvironmentTranslator.getSecretPolicyStatement();
229+
230+
const runtime = nodeVersionMap[props.runtime];
239231

240232
const require = createRequire(import.meta.url);
241-
const bannerCodeFile = require.resolve('./resolve_secret_banner');
242-
const bannerCode = fs
243-
.readFileSync(bannerCodeFile, 'utf-8')
244-
.replaceAll('\n', '')
245-
.replaceAll('\r', '')
246-
.split('//#')[0]; // remove source map
247233

248-
const cjsShimRequire = require.resolve('./cjs_shim');
234+
const shims =
235+
runtime === Runtime.NODEJS_16_X
236+
? // this shim includes the cjs shim because it's required for the v2 sdk to work
237+
[require.resolve('./lambda-shims/resolve_ssm_params_sdk_v2_shim')]
238+
: [
239+
require.resolve('./lambda-shims/cjs_shim'),
240+
require.resolve('./lambda-shims/resolve_ssm_params_shim'),
241+
];
249242

250243
const functionLambda = new NodejsFunction(scope, `${id}-lambda`, {
251244
entry: props.entry,
252-
environment: environmentRecord as { [key: string]: string }, // for some reason TS can't figure out that this is the same as Record<string, string>
253245
timeout: Duration.seconds(props.timeoutSeconds),
254246
memorySize: props.memoryMB,
255247
runtime: nodeVersionMap[props.runtime],
256248
bundling: {
257-
banner: bannerCode,
258249
format: OutputFormat.ESM,
259-
inject: [cjsShimRequire], // replace require to fix dynamic require errors with cjs
250+
inject: shims,
260251
},
261252
});
262253

263-
if (secretPolicyStatement) {
264-
functionLambda.grantPrincipal.addToPrincipalPolicy(secretPolicyStatement);
265-
}
254+
new FunctionEnvironmentTranslator(
255+
functionLambda,
256+
props['environment'],
257+
backendSecretResolver
258+
);
266259

267260
this.resources = {
268261
lambda: functionLambda,

packages/backend-function/src/function_env_translator.test.ts

Lines changed: 135 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import {
77
BackendSecretResolver,
88
ResolvePathResult,
99
} from '@aws-amplify/plugin-types';
10-
import { SecretValue } from 'aws-cdk-lib';
10+
import { App, SecretValue, Stack } from 'aws-cdk-lib';
1111
import assert from 'node:assert';
1212
import { ParameterPathConversions } from '@aws-amplify/platform-core';
13+
import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda';
14+
import { Template } from 'aws-cdk-lib/assertions';
1315

1416
const testStack = {} as Construct;
1517

@@ -47,23 +49,31 @@ class TestBackendSecret implements BackendSecret {
4749
};
4850
}
4951

50-
void describe('functionEnvironmentTranslator', () => {
52+
void describe('FunctionEnvironmentTranslator', () => {
5153
const backendResolver = new TestBackendSecretResolver();
5254

5355
void it('translates env props that do not contain secrets', () => {
5456
const functionEnvProp = {
5557
TEST_VAR: 'testValue',
5658
};
5759

58-
const functionEnvironmentTranslator = new FunctionEnvironmentTranslator(
59-
testStack,
60+
const testLambda = getTestLambda();
61+
62+
new FunctionEnvironmentTranslator(
63+
testLambda,
6064
functionEnvProp,
6165
backendResolver
6266
);
6367

64-
assert.deepEqual(functionEnvironmentTranslator.getEnvironmentRecord(), {
65-
AMPLIFY_SECRET_PATHS: '{}',
66-
TEST_VAR: 'testValue',
68+
const template = Template.fromStack(Stack.of(testLambda));
69+
template.resourceCountIs('AWS::Lambda::Function', 1);
70+
template.hasResourceProperties('AWS::Lambda::Function', {
71+
Environment: {
72+
Variables: {
73+
AMPLIFY_SSM_ENV_CONFIG: '{}',
74+
TEST_VAR: 'testValue',
75+
},
76+
},
6777
});
6878
});
6979

@@ -73,35 +83,141 @@ void describe('functionEnvironmentTranslator', () => {
7383
TEST_SECRET: new TestBackendSecret('secretValue'),
7484
};
7585

76-
const functionEnvironmentTranslator = new FunctionEnvironmentTranslator(
77-
testStack,
86+
const testLambda = getTestLambda();
87+
88+
new FunctionEnvironmentTranslator(
89+
testLambda,
7890
functionEnvProp,
7991
backendResolver
8092
);
8193

82-
assert.deepEqual(functionEnvironmentTranslator.getEnvironmentRecord(), {
83-
AMPLIFY_SECRET_PATHS: JSON.stringify({
84-
'/amplify/testBackendId/testBranchName-branch-e482a1c36f/secretValue': {
85-
name: 'TEST_SECRET',
86-
sharedPath: '/amplify/shared/testBackendId/secretValue',
94+
const template = Template.fromStack(Stack.of(testLambda));
95+
96+
template.resourceCountIs('AWS::Lambda::Function', 1);
97+
template.hasResourceProperties('AWS::Lambda::Function', {
98+
Environment: {
99+
Variables: {
100+
AMPLIFY_SSM_ENV_CONFIG: JSON.stringify({
101+
'/amplify/testBackendId/testBranchName-branch-e482a1c36f/secretValue':
102+
{
103+
name: 'TEST_SECRET',
104+
sharedPath: '/amplify/shared/testBackendId/secretValue',
105+
},
106+
}),
107+
TEST_SECRET: '<value will be resolved during runtime>',
108+
TEST_VAR: 'testValue',
87109
},
88-
}),
89-
TEST_SECRET: '<value will be resolved during runtime>',
90-
TEST_VAR: 'testValue',
110+
},
91111
});
92112
});
93113

94114
void it('throws if function prop contains a reserved env name', () => {
95115
const functionEnvProp = {
96-
AMPLIFY_SECRET_PATHS: 'test',
116+
AMPLIFY_SSM_ENV_CONFIG: 'test',
97117
};
118+
98119
assert.throws(
99120
() =>
100121
new FunctionEnvironmentTranslator(
101-
testStack,
122+
getTestLambda(),
102123
functionEnvProp,
103124
backendResolver
104125
)
105126
);
106127
});
128+
129+
void it('does not add SSM policy if no ssm paths are present', () => {
130+
const functionEnvProp = {
131+
TEST_VAR: 'testValue',
132+
};
133+
134+
const testLambda = getTestLambda();
135+
136+
new FunctionEnvironmentTranslator(
137+
testLambda,
138+
functionEnvProp,
139+
backendResolver
140+
);
141+
142+
const template = Template.fromStack(Stack.of(testLambda));
143+
template.resourceCountIs('AWS::IAM::Policy', 0);
144+
});
145+
146+
void it('grants SSM read permissions for secret paths', () => {
147+
const functionEnvProp = {
148+
TEST_VAR: 'testValue',
149+
TEST_SECRET: new TestBackendSecret('secretValue'),
150+
};
151+
152+
const testLambda = getTestLambda();
153+
154+
new FunctionEnvironmentTranslator(
155+
testLambda,
156+
functionEnvProp,
157+
backendResolver
158+
);
159+
160+
const template = Template.fromStack(Stack.of(testLambda));
161+
162+
template.resourceCountIs('AWS::IAM::Policy', 1);
163+
template.hasResourceProperties('AWS::IAM::Policy', {
164+
PolicyDocument: {
165+
Statement: [
166+
{
167+
Effect: 'Allow',
168+
Action: 'ssm:GetParameters',
169+
Resource: [
170+
{
171+
'Fn::Join': [
172+
'',
173+
[
174+
'arn:',
175+
{
176+
Ref: 'AWS::Partition',
177+
},
178+
':ssm:',
179+
{
180+
Ref: 'AWS::Region',
181+
},
182+
':',
183+
{
184+
Ref: 'AWS::AccountId',
185+
},
186+
':parameter/amplify/testBackendId/testBranchName-branch-e482a1c36f/secretValue',
187+
],
188+
],
189+
},
190+
{
191+
'Fn::Join': [
192+
'',
193+
[
194+
'arn:',
195+
{
196+
Ref: 'AWS::Partition',
197+
},
198+
':ssm:',
199+
{
200+
Ref: 'AWS::Region',
201+
},
202+
':',
203+
{
204+
Ref: 'AWS::AccountId',
205+
},
206+
':parameter/amplify/shared/testBackendId/secretValue',
207+
],
208+
],
209+
},
210+
],
211+
},
212+
],
213+
},
214+
});
215+
});
107216
});
217+
218+
const getTestLambda = () =>
219+
new Function(new Stack(new App()), 'testFunction', {
220+
code: Code.fromInline('test code'),
221+
runtime: Runtime.NODEJS_20_X,
222+
handler: 'handler',
223+
});

0 commit comments

Comments
 (0)