Skip to content

Commit 59d2d0f

Browse files
committed
add option to associate exported backend with amplify app
- if given an amplifyAppId and amplifyEnvironment, then created the associated backend environment - ensure amplify environment name is valid - ensure exception tests are properly accounted for - add more details for AmplifyExportedBackendProps - make AmplifyCategoryNotFoundError a little more clear - emulate root stack cfn parameters that cli uses
1 parent 7dc9e8a commit 59d2d0f

File tree

10 files changed

+220
-49
lines changed

10 files changed

+220
-49
lines changed

API.md

+35-7
Original file line numberDiff line numberDiff line change
@@ -317,28 +317,56 @@ Whether to enable termination protection for this stack.
317317

318318
---
319319

320-
##### `amplifyEnvironment`<sup>Required</sup> <a name="@aws-amplify/cdk-exported-backend.AmplifyExportedBackendProps.property.amplifyEnvironment"></a>
320+
##### `path`<sup>Required</sup> <a name="@aws-amplify/cdk-exported-backend.AmplifyExportedBackendProps.property.path"></a>
321321

322322
```typescript
323-
public readonly amplifyEnvironment: string;
323+
public readonly path: string;
324324
```
325325

326326
- *Type:* `string`
327-
- *Default:* is 'dev'
328327

329-
The Amplify CLI environment deploy to The amplify backend requires a stage to deploy.
328+
The path to the exported folder that contains the artifacts for the Amplify CLI backend ex: ./amplify-synth-out/.
330329

331330
---
332331

333-
##### `path`<sup>Required</sup> <a name="@aws-amplify/cdk-exported-backend.AmplifyExportedBackendProps.property.path"></a>
332+
##### `amplifyAppId`<sup>Optional</sup> <a name="@aws-amplify/cdk-exported-backend.AmplifyExportedBackendProps.property.amplifyAppId"></a>
334333

335334
```typescript
336-
public readonly path: string;
335+
public readonly amplifyAppId: string;
337336
```
338337

339338
- *Type:* `string`
340339

341-
The path to the exported folder that contains the artifacts for the Amplify CLI backend ex: ./amplify-synth-out/.
340+
The Amplify App ID to which the new backend environment will be added.
341+
342+
If the Amplify environment is created and managed by CDK exclusively
343+
then provide an AmplifyAppId to ensure the backend environment
344+
shows up in the AWS Amplify App homepage.
345+
346+
If the Amplify environment is created via Amplify CLI, do not
347+
provide an AmplifyAppId. Trying to create an Amplify backend
348+
via CDK which has already been created by the Amplify CLI will result
349+
in the CDK failing to create the backend and automatically deleting
350+
the existing backend when it deletes the Amplify environment it failed
351+
to deploy.
352+
353+
---
354+
355+
##### `amplifyEnvironment`<sup>Optional</sup> <a name="@aws-amplify/cdk-exported-backend.AmplifyExportedBackendProps.property.amplifyEnvironment"></a>
356+
357+
```typescript
358+
public readonly amplifyEnvironment: string;
359+
```
360+
361+
- *Type:* `string`
362+
- *Default:* is 'dev'
363+
364+
An environment name to contain Amplify CLI backend resources.
365+
366+
An Amplify backend is a collection of various AWS
367+
resources organized into categories (api, function, custom, etc) which are deployed
368+
together into an environment. Environments sometimes reflect deployment
369+
stages such as 'dev', 'test', and 'prod'.
342370

343371
---
344372

README.md

+7-6
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ Add to your CDK app:
3232
import { AmplifyExportedBackend } from '@aws-amplify/cdk-exported-backend';
3333
...
3434
const amplifyExport = new AmplifyExportedBackend(app, 'AmplifyExportedBackend', {
35-
path: './amplify-export-myAmplifyApp',
36-
amplifyEnvironment: 'dev',
35+
path: './amplify-export-myAmplifyApp'
3736
});
3837

3938

@@ -45,10 +44,12 @@ const amplifyExport = new AmplifyExportedBackend(app, 'AmplifyExportedBackend',
4544

4645
The construct props extend [stack props](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.StackProps.html) and can be used to override the root stack properties.
4746

48-
|Name |Type |Description |Required |Default |
49-
|--- |--- |--- |--- |--- |
50-
|path |String |You can use the absolute or the relative path to the location of the folder. When using relative paths it's important to note that the path is relative to the root of your CDK application |Yes |undefined |
51-
|stage |String |This works similar to Amplify CLI's environment names. The construct makes modification to be able to integrate into the CDK app. |Yes | undefined |
47+
| Name | Type | Description | Required | Default |
48+
|--------------------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-----------|
49+
| path | String | You can use the absolute or the relative path to the location of the folder. When using relative paths it's important to note that the path is relative to the root of your CDK application. | Yes | undefined |
50+
| amplifyEnvironment | String | The construct emulates Amplify CLI's environment name convention to integrate Amplify exported resources into the CDK app. | No | dev |
51+
| amplifyAppId | String | Create an Amplify backend environment for the specified Amplify App Id. Do not provide an amplifyAppId if the environment was already added by the Amplify CLI. | No | undefined |
52+
5253

5354

5455
Deploy this to your account

src/amplify-exported-backend-props.ts

+27-7
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,36 @@
11
import { StackProps } from 'aws-cdk-lib';
22

33
export interface AmplifyExportedBackendProps extends StackProps {
4-
/**
5-
* The Amplify CLI environment deploy to
6-
* The amplify backend requires a stage to deploy
7-
* @default is 'dev'
8-
*/
9-
readonly amplifyEnvironment: string;
10-
114
/**
125
* The path to the exported folder that contains the artifacts for the Amplify CLI backend
136
* ex: ./amplify-synth-out/
147
*/
158
readonly path: string;
9+
10+
/**
11+
* The Amplify App ID to which the new backend environment will be added.
12+
*
13+
* If the Amplify environment is created and managed by CDK exclusively
14+
* then provide an AmplifyAppId to ensure the backend environment
15+
* shows up in the AWS Amplify App homepage.
16+
*
17+
* If the Amplify environment is created via Amplify CLI, do not
18+
* provide an AmplifyAppId. Trying to create an Amplify backend
19+
* via CDK which has already been created by the Amplify CLI will result
20+
* in the CDK failing to create the backend and automatically deleting
21+
* the existing backend when it deletes the Amplify environment it failed
22+
* to deploy.
23+
*/
24+
readonly amplifyAppId?: string
25+
26+
/**
27+
* An environment name to contain Amplify CLI backend resources.
28+
*
29+
* An Amplify backend is a collection of various AWS
30+
* resources organized into categories (api, function, custom, etc) which are deployed
31+
* together into an environment. Environments sometimes reflect deployment
32+
* stages such as 'dev', 'test', and 'prod'.
33+
* @default is 'dev'
34+
*/
35+
readonly amplifyEnvironment?: string;
1636
}

src/base-exported-backend.ts

+17-9
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,18 @@ const {
1414
AMPLIFY_EXPORT_TAG_FILE,
1515
AMPLIFY_CATEGORY_MAPPING_FILE,
1616
} = Constants;
17+
18+
// conform to Amplify conventions: https://github.com/aws-amplify/amplify-cli/blob/v12.0.3/packages/amplify-cli/src/init-steps/s0-analyzeProject.ts#L206-L208
19+
const isEnvNameValid = (inputEnvName: string): boolean => /^[a-z]{2,10}$/.test(inputEnvName);
20+
21+
function validateEnv(env: string = 'dev') {
22+
if(!isEnvNameValid(env)) throw new Error(`param 'amplifyEnvironment' must be between 2 and 10 characters, and lowercase only. Got '${env}'.`);
23+
return env;
24+
}
25+
1726
class AmplifyCategoryNotFoundError extends Error {
1827
constructor(category: string, service?: string) {
19-
super(`The category: ${category} ${service ? 'of service: ' + service: '' } not found.`);
28+
super(`${service ? `service '${service}' of category '${category}'` : `category '${category}'`} not found`);
2029
}
2130
}
2231

@@ -30,16 +39,19 @@ export class BaseAmplifyExportedBackend extends Construct {
3039
protected exportBackendManifest: ExportManifest;
3140
protected exportTags: ExportTag[];
3241
protected auxiliaryDeployment?: BucketDeployment;
33-
protected env?: string
42+
protected env: string;
43+
protected appId?: string;
3444
constructor(
3545
scope: Construct,
3646
id: string,
3747
exportPath: string,
38-
amplifyEnvironment: string,
48+
amplifyEnvironment?: string,
49+
amplifyAppId?: string
3950
) {
4051
super(scope, id);
4152

42-
this.env = amplifyEnvironment;
53+
this.env = validateEnv(amplifyEnvironment);
54+
this.appId = amplifyAppId;
4355
this.exportPath = this.validatePath(exportPath);
4456

4557

@@ -140,11 +152,7 @@ export class BaseAmplifyExportedBackend extends Construct {
140152

141153
private modifyEnv(nameWithEnv: string): string {
142154
let splitValues = nameWithEnv.split('-');
143-
if (this.env) {
144-
splitValues[2] = this.env;
145-
} else {
146-
splitValues.splice(2, 1);
147-
}
155+
splitValues[2] = this.env;
148156
return splitValues.join('-');
149157
}
150158

src/export-backend-asset-handler/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class AmplifyExportAssetHandler extends Construct {
4949
private categoryStackWithDeployment: CategoryStackMappingWithDeployment[];
5050
private exportPath: string;
5151
private rootStack: Stack;
52-
private env?: string;
52+
private env: string;
5353

5454
private auxiliaryDeployment: BucketDeployment | undefined;
5555

src/export-backend.ts

+59-10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import {
1717
LambdaFunctionIncludedNestedStack,
1818
} from './include-nested-stacks/lambda-function/lambda-function-nested-stack';
1919
import { CategoryStackMapping } from './types/category-stack-mapping';
20+
import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from "aws-cdk-lib/custom-resources";
21+
import { CfnParameter } from 'aws-cdk-lib';
22+
import { CfnRole } from 'aws-cdk-lib/aws-iam';
23+
import { CfnBucket } from 'aws-cdk-lib/aws-s3';
2024

2125
const { API_CATEGORY, AUTH_CATEGORY, FUNCTION_CATEGORY } = Constants;
2226

@@ -46,7 +50,7 @@ export class AmplifyExportedBackend
4650
id: string,
4751
props: AmplifyExportedBackendProps,
4852
) {
49-
super(scope, id, props.path, props.amplifyEnvironment);
53+
super(scope, id, props.path, props.amplifyEnvironment, props.amplifyAppId);
5054

5155
this.rootStack = new cdk.Stack(scope, `${id}-amplify-backend-stack`, {
5256
...props,
@@ -59,7 +63,7 @@ export class AmplifyExportedBackend
5963
{
6064
backendPath: props.path,
6165
categoryStackMapping: this.categoryStackMappings,
62-
env: props.amplifyEnvironment ? props.amplifyEnvironment : 'dev',
66+
env: this.env,
6367
exportManifest: this.exportBackendManifest,
6468
},
6569
);
@@ -76,19 +80,65 @@ export class AmplifyExportedBackend
7680

7781
amplifyExportHandler.setDependencies(include);
7882

79-
this.applyTags(this.rootStack, props.amplifyEnvironment);
83+
// used to emulate the input parameters that the Amplify CLI uses
84+
const deploymentBucket = this.cfnInclude.getResource('DeploymentBucket') as CfnBucket;
85+
const authRole = this.cfnInclude.getResource('AuthRole') as CfnRole;
86+
const unauthRole = this.cfnInclude.getResource('UnauthRole') as CfnRole;
87+
new CfnParameter(this.rootStack, 'AuthRoleName', {
88+
type: 'String',
89+
default: authRole.roleName
90+
});
91+
new CfnParameter(this.rootStack, 'DeploymentBucketName', {
92+
type: 'String',
93+
default: deploymentBucket.bucketName
94+
});
95+
new CfnParameter(this.rootStack, 'UnauthRoleName', {
96+
type: 'String',
97+
default: unauthRole.roleName
98+
});
8099

100+
// just like in amplify env add, we add the backend to the amplify application
101+
// and then deploy the CFN. This also ensures the amplify env only deletes when
102+
// the CFN is gone too.
103+
if(this.appId) {
104+
new AwsCustomResource(this.rootStack, 'CreateBackendEnvironment', {
105+
onCreate: {
106+
service: 'Amplify',
107+
action: 'createBackendEnvironment',
108+
parameters: {
109+
appId: this.appId,
110+
environmentName: this.env,
111+
stackName: this.rootStack.stackName,
112+
deploymentArtifacts: deploymentBucket.bucketName,
113+
},
114+
physicalResourceId: PhysicalResourceId.of(`${this.appId}-${this.env}-backendEnvironment`)
115+
},
116+
onDelete: {
117+
service: 'Amplify',
118+
action: 'deleteBackendEnvironment',
119+
parameters: {
120+
appId: this.appId,
121+
environmentName: this.env
122+
}
123+
},
124+
policy: AwsCustomResourcePolicy.fromSdkCalls({
125+
resources: AwsCustomResourcePolicy.ANY_RESOURCE,
126+
}),
127+
});
128+
}
129+
130+
this.applyTags(this.rootStack, this.env);
81131
}
82132

83-
private applyTags(rootStack: cdk.Stack, env: string = 'dev') {
133+
private applyTags(rootStack: cdk.Stack, env: string) {
84134
this.exportTags.forEach((tag) => {
85135
rootStack.tags.setTag(tag.key, tag.value.replace('{project-env}', env));
86136
});
87137
}
88138

89139
/**
90140
* Method to get the auth stack
91-
* @returns the nested stack of type {IAuthIncludeNestedStack}
141+
* @returns the nested stack of type {@link AuthIncludedNestedStack}
92142
* @throws {AmplifyCategoryNotFoundError} if the auth stack doesn't exist
93143
* @method
94144
* @function
@@ -104,8 +154,7 @@ export class AmplifyExportedBackend
104154

105155
/**
106156
* Use this to get the api graphql stack from the backend
107-
* @returns the nested stack of type {IAPIGraphQLIncludeNestedStack}
108-
* @
157+
* @returns the nested stack of type {@link APIGraphQLIncludedNestedStack}
109158
* @throws {AmplifyCategoryNotFoundError} if the API graphql stack doesn't exist
110159
*/
111160
graphqlNestedStacks(): APIGraphQLIncludedNestedStack {
@@ -120,7 +169,7 @@ export class AmplifyExportedBackend
120169

121170
/**
122171
* Use this to get all the lambda functions from the backend
123-
* @returns {ILambdaFunctionIncludedNestedStack[]}
172+
* @returns {LambdaFunctionIncludedNestedStack[]}
124173
* @throws {AmplifyCategoryNotFoundError} if the no Lambda Function stacks are found
125174
*/
126175
lambdaFunctionNestedStacks(): LambdaFunctionIncludedNestedStack[] {
@@ -134,7 +183,7 @@ export class AmplifyExportedBackend
134183

135184
/**
136185
* Use this to get a specific lambda function from the backend
137-
* @returns {ILambdaFunctionIncludedNestedStack}
186+
* @returns {LambdaFunctionIncludedNestedStack}
138187
* @param functionName the function name to get from the nested stack
139188
* @throws {AmplifyCategoryNotFoundError} if the lambda function stack doesn't exist
140189
*/
@@ -172,7 +221,7 @@ export class AmplifyExportedBackend
172221
/**
173222
* Use this to get rest api stack from the backend
174223
* @param resourceName
175-
* @return {IAPIRestIncludedStack} the nested of type Rest API
224+
* @return {APIRestIncludedStack} the nested of type Rest API
176225
* @throws {AmplifyCategoryNotFoundError} if the API Rest stack doesn't exist
177226
*/
178227
apiRestNestedStack(resourceName: string): APIRestIncludedStack {

src/include-nested-stacks/api-rest/api-rest-include-stack.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,18 @@ export class APIRestIncludedStack
1313
/**
1414
* Gets the RestApi of the API stack
1515
* @returns {CfnRestApi}
16+
* @throws {CfnResourceNotFoundError} if not found
1617
*/
1718
restAPI(): CfnRestApi {
1819
return this.getResourceConstruct<CfnRestApi>(this.resourceName);
1920
}
2021

2122

2223
/**
23-
* Gets the Deployment of the Rest API
24-
* @returns {CfnDeployment}
25-
*/
24+
* Gets the Deployment of the Rest API
25+
* @returns {CfnDeployment}
26+
* @throws {CfnResourceNotFoundError} if not found
27+
*/
2628
apiDeployment(): CfnDeployment {
2729
return this.getResourceConstruct<CfnDeployment>(
2830
`DeploymentAPIGW${this.resourceName}`,

src/include-nested-stacks/auth/auth-include-nested-stack.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export interface ProviderCredential {
99
}
1010

1111
export class AuthIncludedNestedStack extends BaseIncludedStack {
12+
/**
13+
* attaches third party auth provider credentials to identity pool
14+
* @throws {CfnResourceNotFoundError} if
15+
*/
1216
hostedUiProviderCredentials(credentials: ProviderCredential[]): void {
1317
const hostedUICustomResourceInputs = this.getResourceConstruct<
1418
CfnCustomResource
@@ -31,14 +35,15 @@ export class AuthIncludedNestedStack extends BaseIncludedStack {
3135
}
3236
/**
3337
* @returns {CfnIdentityPool} of the auth stack
38+
* @throws {CfnResourceNotFoundError} if not found
3439
*/
3540
userPool(): CfnUserPool {
3641
return this.getResourceConstruct<CfnUserPool>('UserPool');
3742
}
3843

3944
/**
4045
* @returns Cognito UserPool {CfnUserPool} of the auth stack
41-
* @throws {}
46+
* @throws {CfnResourceNotFoundError} if not found
4247
*/
4348
identityPool(): CfnIdentityPool {
4449
return this.getResourceConstruct<CfnIdentityPool>('IdentityPool');

src/types/category-stack-mapping.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export interface CategoryStackMapping {
55
readonly category: string;
66
readonly resourceName: string;
77
readonly service: string;
8-
};
8+
}
99

1010
export type CategoryStackMappingWithDeployment = CategoryStackMapping & {
1111
bucketDeployment? :BucketDeployment;

0 commit comments

Comments
 (0)