Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): support CloudFormation simplified resource import #28066

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,65 @@ integTest('deploy without execute a named change set', withDefaultFixture(async
expect(changeSets[0].Status).toEqual('CREATE_COMPLETE');
}));

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('describeStacks', {
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('listChangeSets', {
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('describeStacks', {
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('listChangeSets', {
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('describeStacks', {
StackName: fixture.fullStackName(stackName),
})).rejects.toThrow('does not exist');
}));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for adding this 👍🏼


integTest('security related changes without a CLI are expected to fail', withDefaultFixture(async (fixture) => {
// redirect /dev/null to stdin, which means there will not be tty attached
// since this stack includes security-related changes, the deployment should
Expand Down
26 changes: 26 additions & 0 deletions packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,30 @@ $ 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 pass the `--import-existing-resources` flag to the `deploy` command:

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

This allows you to automatically import the resources in your template that already exist in your AWS account during CloudFormation deployments. With this feature, you can reduce the manual effort of import operations and avoid deployment failures because of naming conflicts.

To review which resources are imported or not before actually executing a change set, use `--method=prepare-change-set` flag. 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
```

To enable this feature only for a specific stack, use `--exclusively` flag.

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

This parameter can only import resources that have custom names in templates. For more information, see [name type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html) in the AWS CloudFormation User Guide. To import resources that do not accept custom names, such as EC2 instances, use the `cdk import` instead. You can find more details [here](https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateChangeSet.html#API_CreateChangeSet_RequestParameters).

#### Hotswap deployments for faster development

You can pass the `--hotswap` flag to the `deploy` command:
Expand Down Expand Up @@ -559,6 +583,8 @@ 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 `--import-existing-resources` option of `cdk deploy` command. This parameter only works for resources that you can set custom physical names, such as S3 bucket, DynamoDB table, etc. For more information, see [`ImportResourceResources` parameter](https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_CreateChangeSet.html#API_CreateChangeSet_RequestParameters) in AWS CloudFormation API reference.

#### 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 @@ -231,6 +231,13 @@ export interface ChangeSetDeploymentMethod {
* If not provided, a name will be generated automatically.
*/
readonly changeSetName?: string;

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

const LARGE_TEMPLATE_SIZE_KB = 50;
Expand Down Expand Up @@ -382,7 +389,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 @@ -413,7 +421,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 @@ -425,6 +433,7 @@ class FullCloudFormationDeployment {
ResourcesToImport: this.options.resourcesToImport,
Description: `CDK Changeset for execution ${this.uuid}`,
ClientToken: `create${this.uuid}`,
ImportExistingResources: importExistingResources,
...this.commonPrepareOptions(),
}).promise();

Expand Down
10 changes: 7 additions & 3 deletions packages/aws-cdk/lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ async function parseCommandLineArguments(args: string[]) {
requiresArg: true,
desc: 'How to perform the deployment. Direct is a bit faster but lacks progress information',
})
.option('import-existing-resources', { type: 'boolean', desc: 'Indicates if the stack set imports resources that already exist.', default: false })
.option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false })
.option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} })
.option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true })
Expand Down Expand Up @@ -547,16 +548,19 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
if (args.changeSetName) {
throw new Error('--change-set-name cannot be used with method=direct');
}
if (args.importExistingResources) {
throw new Error('--import-existing-resources cannot be enabled with method=direct');
}
tmokmss marked this conversation as resolved.
Show resolved Hide resolved
deploymentMethod = { method: 'direct' };
break;
case 'change-set':
deploymentMethod = { method: 'change-set', execute: true, changeSetName: args.changeSetName };
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 };
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 };
deploymentMethod = { method: 'change-set', execute: args.execute ?? true, changeSetName: args.changeSetName, importExistingResources: args.importExistingResources };
break;
}

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 @@ -861,6 +861,41 @@ describe('disable rollback', () => {

});

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

// THEN
expect(cfnMocks.createChangeSet).toHaveBeenCalledTimes(1);
expect(cfnMocks.createChangeSet).toHaveBeenCalledWith(expect.objectContaining({
ImportExistingResources: false,
}));
});

test('import-existing-resources is enabled', async () => {
// WHEN
await deployStack({
...standardDeployStackArguments(),
deploymentMethod: {
method: 'change-set',
importExistingResources: true,
},
});

// THEN
expect(cfnMocks.createChangeSet).toHaveBeenCalledTimes(1);
expect(cfnMocks.createChangeSet).toHaveBeenCalledWith(expect.objectContaining({
ImportExistingResources: true,
}));
});
});

/**
* Set up the mocks so that it looks like the stack exists to start with
*
Expand Down
Loading