Skip to content

Commit

Permalink
feat(cli): support CloudFormation simplified resource import (#32676)
Browse files Browse the repository at this point in the history
### 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*
  • Loading branch information
kaizencc authored Jan 1, 2025
1 parent a0f99cc commit ca33f0a
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,65 @@ integTest(
}),
);

integTest('deploy with import-existing-resources true', withDefaultFixture(async (fixture) => {
const stackArn = await fixture.cdkDeploy('test-2', {
options: ['--no-execute', '--import-existing-resources'],
captureStderr: false,
});
// verify that we only deployed a single stack (there's a single ARN in the output)
expect(stackArn.split('\n').length).toEqual(1);

const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({
StackName: stackArn,
}));
expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS');

// verify a change set was successfully created
// Here, we do not test whether a resource is actually imported, because that is a CloudFormation feature, not a CDK feature.
const changeSetResponse = await fixture.aws.cloudFormation.send(new ListChangeSetsCommand({
StackName: stackArn,
}));
const changeSets = changeSetResponse.Summaries || [];
expect(changeSets.length).toEqual(1);
expect(changeSets[0].Status).toEqual('CREATE_COMPLETE');
expect(changeSets[0].ImportExistingResources).toEqual(true);
}));

integTest('deploy without import-existing-resources', withDefaultFixture(async (fixture) => {
const stackArn = await fixture.cdkDeploy('test-2', {
options: ['--no-execute'],
captureStderr: false,
});
// verify that we only deployed a single stack (there's a single ARN in the output)
expect(stackArn.split('\n').length).toEqual(1);

const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({
StackName: stackArn,
}));
expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS');

// verify a change set was successfully created and ImportExistingResources = false
const changeSetResponse = await fixture.aws.cloudFormation.send(new ListChangeSetsCommand({
StackName: stackArn,
}));
const changeSets = changeSetResponse.Summaries || [];
expect(changeSets.length).toEqual(1);
expect(changeSets[0].Status).toEqual('CREATE_COMPLETE');
expect(changeSets[0].ImportExistingResources).toEqual(false);
}));

integTest('deploy with method=direct and import-existing-resources fails', withDefaultFixture(async (fixture) => {
const stackName = 'iam-test';
await expect(fixture.cdkDeploy(stackName, {
options: ['--import-existing-resources', '--method=direct'],
})).rejects.toThrow('exited with error');

// Ensure stack was not deployed
await expect(fixture.aws.cloudFormation.send(new DescribeStacksCommand({
StackName: fixture.fullStackName(stackName),
}))).rejects.toThrow('does not exist');
}));

integTest(
'update to stack in ROLLBACK_COMPLETE state will delete stack and create a new one',
withDefaultFixture(async (fixture) => {
Expand Down
40 changes: 40 additions & 0 deletions packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,41 @@ $ cdk deploy --method=prepare-change-set --change-set-name MyChangeSetName
For more control over when stack changes are deployed, the CDK can generate a
CloudFormation change set but not execute it.

#### Import existing resources

You can utilize the AWS CloudFormation
[feature](https://aws.amazon.com/about-aws/whats-new/2023/11/aws-cloudformation-import-parameter-changesets/)
that automatically imports resources in your template that already exist in your account.
To do so, pass the `--import-existing-resources` flag to the `deploy` command:

```console
$ cdk deploy --import-existing-resources
```

This automatically imports resources in your CDK application that represent
unmanaged resources in your account. It reduces the manual effort of import operations and
avoids deployment failures due to naming conflicts with unmanaged resources in your account.

Use the `--method=prepare-change-set` flag to review which resources are imported or not before deploying a changeset.
You can inspect the change set created by CDK from the management console or other external tools.

```console
$ cdk deploy --import-existing-resources --method=prepare-change-set
```

Use the `--exclusively` flag to enable this feature for a specific stack.

```console
$ cdk deploy --import-existing-resources --exclusively StackName
```

Only resources that have custom names can be imported using `--import-existing-resources`.
For more information, see [name type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html).
To import resources that do not accept custom names, such as EC2 instances,
use the `cdk import` instead.
Visit [Bringing existing resources into CloudFormation management](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import.html)
for more details.

#### Ignore No Stacks

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

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

#### Limitations

This feature currently has the following limitations:
Expand Down
13 changes: 11 additions & 2 deletions packages/aws-cdk/lib/api/deploy-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,13 @@ export interface ChangeSetDeploymentMethod {
* If not provided, a name will be generated automatically.
*/
readonly changeSetName?: string;

/**
* Indicates if the change set imports resources that already exist.
*
* @default false
*/
readonly importExistingResources?: boolean;
}

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

if (changeSetHasNoChanges(changeSetDescription)) {
Expand Down Expand Up @@ -525,7 +533,7 @@ class FullCloudFormationDeployment {
return this.executeChangeSet(changeSetDescription);
}

private async createChangeSet(changeSetName: string, willExecute: boolean) {
private async createChangeSet(changeSetName: string, willExecute: boolean, importExistingResources: boolean) {
await this.cleanupOldChangeset(changeSetName);

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

Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/cli-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,13 @@ export interface DeployOptions {
*/
readonly method?: string;

/**
* Indicates if the stack set imports resources that already exist.
*
* @default - false
*/
readonly importExistingResources?: boolean;

/**
* Always deploy stack even if templates are identical
*
Expand Down
6 changes: 6 additions & 0 deletions packages/aws-cdk/lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,27 +292,33 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
if (args.changeSetName) {
throw new ToolkitError('--change-set-name cannot be used with method=direct');
}
if (args.importExistingResources) {
throw new Error('--import-existing-resources cannot be enabled with method=direct');
}
deploymentMethod = { method: 'direct' };
break;
case 'change-set':
deploymentMethod = {
method: 'change-set',
execute: true,
changeSetName: args.changeSetName,
importExistingResources: args.importExistingResources,
};
break;
case 'prepare-change-set':
deploymentMethod = {
method: 'change-set',
execute: false,
changeSetName: args.changeSetName,
importExistingResources: args.importExistingResources,
};
break;
case undefined:
deploymentMethod = {
method: 'change-set',
execute: args.execute ?? true,
changeSetName: args.changeSetName,
importExistingResources: args.importExistingResources,
};
break;
}
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export async function makeConfig(): Promise<CliConfig> {
requiresArg: true,
desc: 'How to perform the deployment. Direct is a bit faster but lacks progress information',
},
'import-existing-resources': { type: 'boolean', desc: 'Indicates if the stack set imports resources that already exist.', default: false },
'force': { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false },
'parameters': { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', default: {} },
'outputs-file': { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true },
Expand Down
5 changes: 5 additions & 0 deletions packages/aws-cdk/lib/parse-command-line-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,11 @@ export function parseCommandLineArguments(args: Array<string>): any {
requiresArg: true,
desc: 'How to perform the deployment. Direct is a bit faster but lacks progress information',
})
.option('import-existing-resources', {
default: false,
type: 'boolean',
desc: 'Indicates if the stack set imports resources that already exist.',
})
.option('force', {
default: false,
alias: 'f',
Expand Down
35 changes: 35 additions & 0 deletions packages/aws-cdk/test/api/deploy-stack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,41 @@ describe('disable rollback', () => {
});
});

describe('import-existing-resources', () => {
test('is disabled by default', async () => {
// WHEN
await deployStack({
...standardDeployStackArguments(),
deploymentMethod: {
method: 'change-set',
},
});

// THEN
expect(mockCloudFormationClient).toHaveReceivedCommandWith(CreateChangeSetCommand, {
...expect.anything,
ImportExistingResources: false,
} as CreateChangeSetCommandInput);
});

test('is added to the CreateChangeSetCommandInput', async () => {
// WHEN
await deployStack({
...standardDeployStackArguments(),
deploymentMethod: {
method: 'change-set',
importExistingResources: true,
},
});

// THEN
expect(mockCloudFormationClient).toHaveReceivedCommandWith(CreateChangeSetCommand, {
...expect.anything,
ImportExistingResources: true,
} as CreateChangeSetCommandInput);
});
});

test.each([
// From a failed state, a --no-rollback is possible as long as there is not a replacement
[StackStatus.UPDATE_FAILED, 'no-rollback', 'no-replacement', 'did-deploy-stack'],
Expand Down

0 comments on commit ca33f0a

Please sign in to comment.