Skip to content

Commit ca33f0a

Browse files
authored
feat(cli): support CloudFormation simplified resource import (#32676)
### Issue # (if applicable) Closes #28060. ### Reason for this change This feature allows to automatically import exsting resources with the same physical name, such as S3 bucket, DDB table, etc, during a CFn deployment. Because resource import is a vital feature for CDK users e.g. to refactor a construct tree, cdk migrate, etc, it would benefit many potential users if cdk natively support it. ### Description of changes This PR adds a CLI option --import-exsting-resources: boolean to cdk deploy command and pass it to createChangeSet API call. ### Description of how you validated changes Added a cli integ test. ### Checklist - [X] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) Co-authored-by: Masashi Tomooka [[email protected]](mailto:[email protected]) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent a0f99cc commit ca33f0a

File tree

8 files changed

+164
-2
lines changed

8 files changed

+164
-2
lines changed

packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts

+59
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,65 @@ integTest(
487487
}),
488488
);
489489

490+
integTest('deploy with import-existing-resources true', withDefaultFixture(async (fixture) => {
491+
const stackArn = await fixture.cdkDeploy('test-2', {
492+
options: ['--no-execute', '--import-existing-resources'],
493+
captureStderr: false,
494+
});
495+
// verify that we only deployed a single stack (there's a single ARN in the output)
496+
expect(stackArn.split('\n').length).toEqual(1);
497+
498+
const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({
499+
StackName: stackArn,
500+
}));
501+
expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS');
502+
503+
// verify a change set was successfully created
504+
// Here, we do not test whether a resource is actually imported, because that is a CloudFormation feature, not a CDK feature.
505+
const changeSetResponse = await fixture.aws.cloudFormation.send(new ListChangeSetsCommand({
506+
StackName: stackArn,
507+
}));
508+
const changeSets = changeSetResponse.Summaries || [];
509+
expect(changeSets.length).toEqual(1);
510+
expect(changeSets[0].Status).toEqual('CREATE_COMPLETE');
511+
expect(changeSets[0].ImportExistingResources).toEqual(true);
512+
}));
513+
514+
integTest('deploy without import-existing-resources', withDefaultFixture(async (fixture) => {
515+
const stackArn = await fixture.cdkDeploy('test-2', {
516+
options: ['--no-execute'],
517+
captureStderr: false,
518+
});
519+
// verify that we only deployed a single stack (there's a single ARN in the output)
520+
expect(stackArn.split('\n').length).toEqual(1);
521+
522+
const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({
523+
StackName: stackArn,
524+
}));
525+
expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS');
526+
527+
// verify a change set was successfully created and ImportExistingResources = false
528+
const changeSetResponse = await fixture.aws.cloudFormation.send(new ListChangeSetsCommand({
529+
StackName: stackArn,
530+
}));
531+
const changeSets = changeSetResponse.Summaries || [];
532+
expect(changeSets.length).toEqual(1);
533+
expect(changeSets[0].Status).toEqual('CREATE_COMPLETE');
534+
expect(changeSets[0].ImportExistingResources).toEqual(false);
535+
}));
536+
537+
integTest('deploy with method=direct and import-existing-resources fails', withDefaultFixture(async (fixture) => {
538+
const stackName = 'iam-test';
539+
await expect(fixture.cdkDeploy(stackName, {
540+
options: ['--import-existing-resources', '--method=direct'],
541+
})).rejects.toThrow('exited with error');
542+
543+
// Ensure stack was not deployed
544+
await expect(fixture.aws.cloudFormation.send(new DescribeStacksCommand({
545+
StackName: fixture.fullStackName(stackName),
546+
}))).rejects.toThrow('does not exist');
547+
}));
548+
490549
integTest(
491550
'update to stack in ROLLBACK_COMPLETE state will delete stack and create a new one',
492551
withDefaultFixture(async (fixture) => {

packages/aws-cdk/README.md

+40
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,41 @@ $ cdk deploy --method=prepare-change-set --change-set-name MyChangeSetName
397397
For more control over when stack changes are deployed, the CDK can generate a
398398
CloudFormation change set but not execute it.
399399

400+
#### Import existing resources
401+
402+
You can utilize the AWS CloudFormation
403+
[feature](https://aws.amazon.com/about-aws/whats-new/2023/11/aws-cloudformation-import-parameter-changesets/)
404+
that automatically imports resources in your template that already exist in your account.
405+
To do so, pass the `--import-existing-resources` flag to the `deploy` command:
406+
407+
```console
408+
$ cdk deploy --import-existing-resources
409+
```
410+
411+
This automatically imports resources in your CDK application that represent
412+
unmanaged resources in your account. It reduces the manual effort of import operations and
413+
avoids deployment failures due to naming conflicts with unmanaged resources in your account.
414+
415+
Use the `--method=prepare-change-set` flag to review which resources are imported or not before deploying a changeset.
416+
You can inspect the change set created by CDK from the management console or other external tools.
417+
418+
```console
419+
$ cdk deploy --import-existing-resources --method=prepare-change-set
420+
```
421+
422+
Use the `--exclusively` flag to enable this feature for a specific stack.
423+
424+
```console
425+
$ cdk deploy --import-existing-resources --exclusively StackName
426+
```
427+
428+
Only resources that have custom names can be imported using `--import-existing-resources`.
429+
For more information, see [name type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html).
430+
To import resources that do not accept custom names, such as EC2 instances,
431+
use the `cdk import` instead.
432+
Visit [Bringing existing resources into CloudFormation management](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import.html)
433+
for more details.
434+
400435
#### Ignore No Stacks
401436

402437
You may have an app with multiple environments, e.g., dev and prod. When starting
@@ -619,6 +654,11 @@ To import an existing resource to a CDK stack, follow the following steps:
619654
5. When `cdk import` reports success, the resource is managed by CDK. Any subsequent
620655
changes in the construct configuration will be reflected on the resource.
621656

657+
NOTE: You can also import existing resources by passing `--import-existing-resources` to `cdk deploy`.
658+
This parameter only works for resources that support custom physical names,
659+
such as S3 Buckets, DynamoDB Tables, etc...
660+
For more information, see [Request Parameters](https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateChangeSet.html#API_CreateChangeSet_RequestParameters).
661+
622662
#### Limitations
623663

624664
This feature currently has the following limitations:

packages/aws-cdk/lib/api/deploy-stack.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,13 @@ export interface ChangeSetDeploymentMethod {
270270
* If not provided, a name will be generated automatically.
271271
*/
272272
readonly changeSetName?: string;
273+
274+
/**
275+
* Indicates if the change set imports resources that already exist.
276+
*
277+
* @default false
278+
*/
279+
readonly importExistingResources?: boolean;
273280
}
274281

275282
export async function deployStack(options: DeployStackOptions): Promise<DeployStackResult> {
@@ -462,7 +469,8 @@ class FullCloudFormationDeployment {
462469
private async changeSetDeployment(deploymentMethod: ChangeSetDeploymentMethod): Promise<DeployStackResult> {
463470
const changeSetName = deploymentMethod.changeSetName ?? 'cdk-deploy-change-set';
464471
const execute = deploymentMethod.execute ?? true;
465-
const changeSetDescription = await this.createChangeSet(changeSetName, execute);
472+
const importExistingResources = deploymentMethod.importExistingResources ?? false;
473+
const changeSetDescription = await this.createChangeSet(changeSetName, execute, importExistingResources);
466474
await this.updateTerminationProtection();
467475

468476
if (changeSetHasNoChanges(changeSetDescription)) {
@@ -525,7 +533,7 @@ class FullCloudFormationDeployment {
525533
return this.executeChangeSet(changeSetDescription);
526534
}
527535

528-
private async createChangeSet(changeSetName: string, willExecute: boolean) {
536+
private async createChangeSet(changeSetName: string, willExecute: boolean, importExistingResources: boolean) {
529537
await this.cleanupOldChangeset(changeSetName);
530538

531539
debug(`Attempting to create ChangeSet with name ${changeSetName} to ${this.verb} stack ${this.stackName}`);
@@ -537,6 +545,7 @@ class FullCloudFormationDeployment {
537545
ResourcesToImport: this.options.resourcesToImport,
538546
Description: `CDK Changeset for execution ${this.uuid}`,
539547
ClientToken: `create${this.uuid}`,
548+
ImportExistingResources: importExistingResources,
540549
...this.commonPrepareOptions(),
541550
});
542551

packages/aws-cdk/lib/cli-arguments.ts

+7
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,13 @@ export interface DeployOptions {
633633
*/
634634
readonly method?: string;
635635

636+
/**
637+
* Indicates if the stack set imports resources that already exist.
638+
*
639+
* @default - false
640+
*/
641+
readonly importExistingResources?: boolean;
642+
636643
/**
637644
* Always deploy stack even if templates are identical
638645
*

packages/aws-cdk/lib/cli.ts

+6
Original file line numberDiff line numberDiff line change
@@ -292,27 +292,33 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
292292
if (args.changeSetName) {
293293
throw new ToolkitError('--change-set-name cannot be used with method=direct');
294294
}
295+
if (args.importExistingResources) {
296+
throw new Error('--import-existing-resources cannot be enabled with method=direct');
297+
}
295298
deploymentMethod = { method: 'direct' };
296299
break;
297300
case 'change-set':
298301
deploymentMethod = {
299302
method: 'change-set',
300303
execute: true,
301304
changeSetName: args.changeSetName,
305+
importExistingResources: args.importExistingResources,
302306
};
303307
break;
304308
case 'prepare-change-set':
305309
deploymentMethod = {
306310
method: 'change-set',
307311
execute: false,
308312
changeSetName: args.changeSetName,
313+
importExistingResources: args.importExistingResources,
309314
};
310315
break;
311316
case undefined:
312317
deploymentMethod = {
313318
method: 'change-set',
314319
execute: args.execute ?? true,
315320
changeSetName: args.changeSetName,
321+
importExistingResources: args.importExistingResources,
316322
};
317323
break;
318324
}

packages/aws-cdk/lib/config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export async function makeConfig(): Promise<CliConfig> {
127127
requiresArg: true,
128128
desc: 'How to perform the deployment. Direct is a bit faster but lacks progress information',
129129
},
130+
'import-existing-resources': { type: 'boolean', desc: 'Indicates if the stack set imports resources that already exist.', default: false },
130131
'force': { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false },
131132
'parameters': { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', default: {} },
132133
'outputs-file': { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true },

packages/aws-cdk/lib/parse-command-line-arguments.ts

+5
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,11 @@ export function parseCommandLineArguments(args: Array<string>): any {
401401
requiresArg: true,
402402
desc: 'How to perform the deployment. Direct is a bit faster but lacks progress information',
403403
})
404+
.option('import-existing-resources', {
405+
default: false,
406+
type: 'boolean',
407+
desc: 'Indicates if the stack set imports resources that already exist.',
408+
})
404409
.option('force', {
405410
default: false,
406411
alias: 'f',

packages/aws-cdk/test/api/deploy-stack.test.ts

+35
Original file line numberDiff line numberDiff line change
@@ -1126,6 +1126,41 @@ describe('disable rollback', () => {
11261126
});
11271127
});
11281128

1129+
describe('import-existing-resources', () => {
1130+
test('is disabled by default', async () => {
1131+
// WHEN
1132+
await deployStack({
1133+
...standardDeployStackArguments(),
1134+
deploymentMethod: {
1135+
method: 'change-set',
1136+
},
1137+
});
1138+
1139+
// THEN
1140+
expect(mockCloudFormationClient).toHaveReceivedCommandWith(CreateChangeSetCommand, {
1141+
...expect.anything,
1142+
ImportExistingResources: false,
1143+
} as CreateChangeSetCommandInput);
1144+
});
1145+
1146+
test('is added to the CreateChangeSetCommandInput', async () => {
1147+
// WHEN
1148+
await deployStack({
1149+
...standardDeployStackArguments(),
1150+
deploymentMethod: {
1151+
method: 'change-set',
1152+
importExistingResources: true,
1153+
},
1154+
});
1155+
1156+
// THEN
1157+
expect(mockCloudFormationClient).toHaveReceivedCommandWith(CreateChangeSetCommand, {
1158+
...expect.anything,
1159+
ImportExistingResources: true,
1160+
} as CreateChangeSetCommandInput);
1161+
});
1162+
});
1163+
11291164
test.each([
11301165
// From a failed state, a --no-rollback is possible as long as there is not a replacement
11311166
[StackStatus.UPDATE_FAILED, 'no-rollback', 'no-replacement', 'did-deploy-stack'],

0 commit comments

Comments
 (0)