Skip to content

Commit e0e62bd

Browse files
awslujartpascual
andauthored
fix: consolidate backend secret custom resources (#2011)
* fix: use a single custom resource for fetching secrets * chore: update package-lock.json * chore: add changeset * chore: refactor changes * chore: cleanup * chore: cleanup * chore: cleanup * chore: cleanup * chore: rename resource id * feed pr base sha and ref into envs before scripts (#2168) * feed pr base sha and ref into envs before scripts * removing empty file --------- Co-authored-by: Roshane Pascual <[email protected]>
1 parent 5a47d21 commit e0e62bd

File tree

8 files changed

+119
-80
lines changed

8 files changed

+119
-80
lines changed

.changeset/purple-otters-poke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@aws-amplify/backend': patch
3+
---
4+
5+
Backend Secrets now use a single custom resource to reduce concurrent lambda executions.

.changeset/spicy-rules-speak.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

packages/backend/src/engine/backend-secret/backend_secret.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class CfnTokenBackendSecret implements BackendSecret {
1616
* The name of the secret to fetch.
1717
*/
1818
constructor(
19-
private readonly name: string,
19+
private readonly secretName: string,
2020
private readonly secretResourceFactory: BackendSecretFetcherFactory
2121
) {}
2222
/**
@@ -28,11 +28,11 @@ export class CfnTokenBackendSecret implements BackendSecret {
2828
): SecretValue => {
2929
const secretResource = this.secretResourceFactory.getOrCreate(
3030
scope,
31-
this.name,
31+
this.secretName,
3232
backendIdentifier
3333
);
3434

35-
const val = secretResource.getAttString('secretValue');
35+
const val = secretResource.getAttString(`${this.secretName}`);
3636
return SecretValue.unsafePlainText(val); // safe since 'val' is a cdk token.
3737
};
3838

@@ -43,11 +43,11 @@ export class CfnTokenBackendSecret implements BackendSecret {
4343
return {
4444
branchSecretPath: ParameterPathConversions.toParameterFullPath(
4545
backendIdentifier,
46-
this.name
46+
this.secretName
4747
),
4848
sharedSecretPath: ParameterPathConversions.toParameterFullPath(
4949
backendIdentifier.namespace,
50-
this.name
50+
this.secretName
5151
),
5252
};
5353
};

packages/backend/src/engine/backend-secret/backend_secret_fetcher_factory.test.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from './backend_secret_fetcher_factory.js';
1010
import { BackendIdentifier } from '@aws-amplify/plugin-types';
1111

12-
const secretResourceType = 'Custom::SecretFetcherResource';
12+
const secretResourceType = 'Custom::AmplifySecretFetcherResource';
1313
const namespace = 'testId';
1414
const name = 'testBranch';
1515
const secretName1 = 'testSecretName1';
@@ -33,26 +33,18 @@ void describe('getOrCreate', () => {
3333
resourceFactory.getOrCreate(stack, secretName2, backendId);
3434

3535
const template = Template.fromStack(stack);
36-
template.resourceCountIs(secretResourceType, 2);
37-
let customResources = template.findResources(secretResourceType, {
36+
// only one custom resource is created that fetches all secrets
37+
template.resourceCountIs(secretResourceType, 1);
38+
const customResources = template.findResources(secretResourceType, {
3839
Properties: {
3940
namespace,
4041
name,
41-
secretName: secretName1,
42+
secretNames: [secretName1, secretName2],
4243
secretLastUpdated,
4344
},
4445
});
4546
assert.equal(Object.keys(customResources).length, 1);
4647

47-
customResources = template.findResources(secretResourceType, {
48-
Properties: {
49-
namespace,
50-
name,
51-
secretName: secretName2,
52-
},
53-
});
54-
assert.equal(Object.keys(customResources).length, 1);
55-
5648
// only 1 secret fetcher lambda and 1 resource provider lambda are created.
5749
const providers = template.findResources('AWS::Lambda::Function');
5850
const names = Object.keys(providers);
@@ -67,6 +59,8 @@ void describe('getOrCreate', () => {
6759
void it('does not create duplicate resource for the same secret name', () => {
6860
const app = new App();
6961
const stack = new Stack(app);
62+
63+
// ensure only 1 resource is created even if this is called twice
7064
resourceFactory.getOrCreate(stack, secretName1, backendId);
7165
resourceFactory.getOrCreate(stack, secretName1, backendId);
7266

@@ -78,6 +72,7 @@ void describe('getOrCreate', () => {
7872
const body = customResources[resourceName]['Properties'];
7973
assert.strictEqual(body['namespace'], namespace);
8074
assert.strictEqual(body['name'], name);
81-
assert.strictEqual(body['secretName'], secretName1);
75+
assert.equal(body['secretNames'].length, 1);
76+
assert.equal(body['secretNames'][0], secretName1);
8277
});
8378
});

packages/backend/src/engine/backend-secret/backend_secret_fetcher_factory.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,38 @@
11
import { Construct } from 'constructs';
22
import { BackendSecretFetcherProviderFactory } from './backend_secret_fetcher_provider_factory.js';
3-
import { CustomResource } from 'aws-cdk-lib';
3+
import { CustomResource, CustomResourceProps, Lazy } from 'aws-cdk-lib';
44
import { BackendIdentifier } from '@aws-amplify/plugin-types';
55
import { SecretResourceProps } from './lambda/backend_secret_fetcher_types.js';
66

77
/**
88
* Resource provider ID for the backend secret resource.
99
*/
10-
export const SECRET_RESOURCE_PROVIDER_ID = 'SecretFetcherResourceProvider';
10+
export const SECRET_RESOURCE_PROVIDER_ID =
11+
'AmplifySecretFetcherResourceProvider';
12+
13+
class SecretFetcherCustomResource extends CustomResource {
14+
private secrets: Set<string>;
15+
constructor(
16+
scope: Construct,
17+
id: string,
18+
props: CustomResourceProps,
19+
secrets: Set<string>
20+
) {
21+
super(scope, id, {
22+
...props,
23+
});
24+
this.secrets = secrets;
25+
}
26+
27+
public addSecret = (secretName: string) => {
28+
this.secrets.add(secretName);
29+
};
30+
}
1131

1232
/**
1333
* Type of the backend custom CFN resource.
1434
*/
15-
const SECRET_RESOURCE_TYPE = `Custom::SecretFetcherResource`;
35+
const SECRET_RESOURCE_TYPE = `Custom::AmplifySecretFetcherResource`;
1636

1737
/**
1838
* The factory to create backend secret-fetcher resource.
@@ -33,15 +53,18 @@ export class BackendSecretFetcherFactory {
3353
scope: Construct,
3454
secretName: string,
3555
backendIdentifier: BackendIdentifier
36-
): CustomResource => {
37-
const secretResourceId = `${secretName}SecretFetcherResource`;
56+
): SecretFetcherCustomResource => {
57+
const secretResourceId = `AmplifySecretFetcherResource`;
3858
const existingResource = scope.node.tryFindChild(
3959
secretResourceId
40-
) as CustomResource;
60+
) as SecretFetcherCustomResource;
4161

4262
if (existingResource) {
63+
existingResource.addSecret(secretName);
4364
return existingResource;
4465
}
66+
const secrets: Set<string> = new Set();
67+
secrets.add(secretName);
4568

4669
const provider = this.secretProviderFactory.getOrCreateInstance(
4770
scope,
@@ -59,16 +82,25 @@ export class BackendSecretFetcherFactory {
5982
namespace: backendIdentifier.namespace,
6083
name: backendIdentifier.name,
6184
type: backendIdentifier.type,
62-
secretName: secretName,
85+
secretNames: Lazy.list({
86+
produce: () => {
87+
return Array.from(secrets);
88+
},
89+
}),
6390
};
6491

65-
return new CustomResource(scope, secretResourceId, {
66-
serviceToken: provider.serviceToken,
67-
properties: {
68-
...customResourceProps,
69-
secretLastUpdated, // this property is only to trigger resource update event.
92+
return new SecretFetcherCustomResource(
93+
scope,
94+
secretResourceId,
95+
{
96+
serviceToken: provider.serviceToken,
97+
properties: {
98+
...customResourceProps,
99+
secretLastUpdated, // this property is only to trigger resource update event.
100+
},
101+
resourceType: SECRET_RESOURCE_TYPE,
70102
},
71-
resourceType: SECRET_RESOURCE_TYPE,
72-
});
103+
secrets
104+
);
73105
};
74106
}

packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const customResourceEventCommon = {
4343
ResourceType: 'AWS::CloudFormation::CustomResource',
4444
ResourceProperties: {
4545
...testBackendIdentifier,
46-
secretName: testSecretName,
46+
secretNames: [testSecretName],
4747
ServiceToken: 'token',
4848
},
4949
OldResourceProperties: {},
@@ -84,7 +84,7 @@ void describe('handleCreateUpdateEvent', () => {
8484
Promise.resolve(testSecret)
8585
);
8686
const val = await handleCreateUpdateEvent(secretHandler, createCfnEvent);
87-
assert.equal(val, testSecretValue);
87+
assert.equal(val[testSecretName], testSecretValue);
8888

8989
assert.equal(mockGetSecret.mock.callCount(), 1);
9090
assert.deepStrictEqual(mockGetSecret.mock.calls[0].arguments, [
@@ -120,7 +120,7 @@ void describe('handleCreateUpdateEvent', () => {
120120
);
121121

122122
const val = await handleCreateUpdateEvent(secretHandler, createCfnEvent);
123-
assert.equal(val, testSecretValue);
123+
assert.equal(val[testSecretName], testSecretValue);
124124

125125
assert.equal(mockGetSecret.mock.callCount(), 2);
126126
assert.deepStrictEqual(mockGetSecret.mock.calls[0].arguments, [
@@ -145,7 +145,7 @@ void describe('handleCreateUpdateEvent', () => {
145145
}
146146
);
147147
const val = await handleCreateUpdateEvent(secretHandler, createCfnEvent);
148-
assert.equal(val, testSecretValue);
148+
assert.equal(val[testSecretName], testSecretValue);
149149

150150
assert.equal(mockGetSecret.mock.callCount(), 2);
151151
assert.deepStrictEqual(mockGetSecret.mock.calls[0].arguments, [

packages/backend/src/engine/backend-secret/lambda/backend_secret_fetcher.ts

Lines changed: 47 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ export const handler = async (
2222

2323
const physicalId =
2424
event.RequestType === 'Create' ? randomUUID() : event.PhysicalResourceId;
25-
let data: { secretValue: string } | undefined = undefined;
25+
let data: Record<string, string> | undefined = undefined;
2626
if (event.RequestType === 'Update' || event.RequestType === 'Create') {
27-
const val = await handleCreateUpdateEvent(secretClient, event);
27+
const secretMap = await handleCreateUpdateEvent(secretClient, event);
2828
data = {
29-
secretValue: val,
29+
...secretMap,
3030
};
3131
}
3232

@@ -47,54 +47,59 @@ export const handler = async (
4747
export const handleCreateUpdateEvent = async (
4848
secretClient: SecretClient,
4949
event: CloudFormationCustomResourceEvent
50-
): Promise<string> => {
50+
): Promise<Record<string, string>> => {
5151
const props = event.ResourceProperties as unknown as SecretResourceProps;
52-
let secret: string | undefined;
52+
const secretMap: Record<string, string> = {};
53+
for (const secretName of props.secretNames) {
54+
let secretValue: string | undefined = undefined;
55+
try {
56+
const resp = await secretClient.getSecret(
57+
{
58+
namespace: props.namespace,
59+
name: props.name,
60+
type: props.type,
61+
},
62+
{
63+
name: secretName,
64+
}
65+
);
66+
secretValue = resp.value;
67+
} catch (err) {
68+
const secretErr = err as SecretError;
69+
if (secretErr.httpStatusCode && secretErr.httpStatusCode >= 500) {
70+
throw new Error(
71+
`Failed to retrieve backend secret '${secretName}' for '${
72+
props.namespace
73+
}/${props.name}'. Reason: ${JSON.stringify(err)}`
74+
);
75+
}
76+
}
5377

54-
try {
55-
const resp = await secretClient.getSecret(
56-
{
57-
namespace: props.namespace,
58-
name: props.name,
59-
type: props.type,
60-
},
61-
{
62-
name: props.secretName,
78+
// if the secret is not available in branch path, try retrieving it at the app-level.
79+
if (!secretValue) {
80+
try {
81+
const resp = await secretClient.getSecret(props.namespace, {
82+
name: secretName,
83+
});
84+
secretValue = resp.value;
85+
} catch (err) {
86+
throw new Error(
87+
`Failed to retrieve backend secret '${secretName}' for '${
88+
props.namespace
89+
}'. Reason: ${JSON.stringify(err)}`
90+
);
6391
}
64-
);
65-
secret = resp?.value;
66-
} catch (err) {
67-
const secretErr = err as SecretError;
68-
if (secretErr.httpStatusCode && secretErr.httpStatusCode >= 500) {
69-
throw new Error(
70-
`Failed to retrieve backend secret '${props.secretName}' for '${
71-
props.namespace
72-
}/${props.name}'. Reason: ${JSON.stringify(err)}`
73-
);
7492
}
75-
}
7693

77-
// if the secret is not available in branch path, try retrieving it at the app-level.
78-
if (!secret) {
79-
try {
80-
const resp = await secretClient.getSecret(props.namespace, {
81-
name: props.secretName,
82-
});
83-
secret = resp?.value;
84-
} catch (err) {
94+
if (!secretValue) {
8595
throw new Error(
86-
`Failed to retrieve backend secret '${props.secretName}' for '${
87-
props.namespace
88-
}'. Reason: ${JSON.stringify(err)}`
96+
`Unable to find backend secret for backend '${props.namespace}', branch '${props.name}', name '${secretName}'`
8997
);
9098
}
91-
}
9299

93-
if (!secret) {
94-
throw new Error(
95-
`Unable to find backend secret for backend '${props.namespace}', branch '${props.name}', name '${props.secretName}'`
96-
);
100+
// store the secret->secretValue pair in the secret map
101+
secretMap[secretName] = secretValue;
97102
}
98103

99-
return secret;
104+
return secretMap;
100105
};
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BackendIdentifier } from '@aws-amplify/plugin-types';
22

33
export type SecretResourceProps = Omit<BackendIdentifier, 'hash'> & {
4-
secretName: string;
4+
secretNames: string[];
55
};

0 commit comments

Comments
 (0)