diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts index 8ebfcc0752716..94bc9e289598e 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts @@ -470,6 +470,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) => { diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 268f0989c6a5e..1d8302085afe1 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -388,6 +388,39 @@ $ 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 +``` + +Automatically import resources in your CDK application which represent +unmanaged resources in your account. +Reduces the manual effort of import operations and avoids +deployment failures due to naming conflicts with unmanaged resources in your account. + +Use `--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 @@ -579,6 +612,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 bucket, DynamoDB table, 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: diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index c9c934dcee4f2..24dad795375cd 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -224,6 +224,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 { @@ -374,7 +381,8 @@ class FullCloudFormationDeployment { private async changeSetDeployment(deploymentMethod: ChangeSetDeploymentMethod): Promise { 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)) { @@ -405,7 +413,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}`); @@ -417,6 +425,7 @@ class FullCloudFormationDeployment { ResourcesToImport: this.options.resourcesToImport, Description: `CDK Changeset for execution ${this.uuid}`, ClientToken: `create${this.uuid}`, + ImportExistingResources: importExistingResources, ...this.commonPrepareOptions(), }).promise(); diff --git a/packages/aws-cdk/lib/cli.ts b/packages/aws-cdk/lib/cli.ts index f63c7cbf21eef..a7fa147af2c76 100644 --- a/packages/aws-cdk/lib/cli.ts +++ b/packages/aws-cdk/lib/cli.ts @@ -131,6 +131,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 }) @@ -576,16 +577,19 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise { }); +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 *