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 0bb02304422f3..f4736584a065f 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 @@ -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) => { diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index cc012275c6274..a824c37697ef0 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -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 @@ -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: diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 38654116cd646..8ec387466a14e 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -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 { @@ -462,7 +469,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)) { @@ -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}`); @@ -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(), }); diff --git a/packages/aws-cdk/lib/cli-arguments.ts b/packages/aws-cdk/lib/cli-arguments.ts index f9c517e2da3fd..126eee3ad4f94 100644 --- a/packages/aws-cdk/lib/cli-arguments.ts +++ b/packages/aws-cdk/lib/cli-arguments.ts @@ -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 * diff --git a/packages/aws-cdk/lib/cli.ts b/packages/aws-cdk/lib/cli.ts index 72344548aff9b..d763eb924b0fc 100644 --- a/packages/aws-cdk/lib/cli.ts +++ b/packages/aws-cdk/lib/cli.ts @@ -292,6 +292,9 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise { 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 }, diff --git a/packages/aws-cdk/lib/parse-command-line-arguments.ts b/packages/aws-cdk/lib/parse-command-line-arguments.ts index 0579aa115d62b..b0d4f3060024e 100644 --- a/packages/aws-cdk/lib/parse-command-line-arguments.ts +++ b/packages/aws-cdk/lib/parse-command-line-arguments.ts @@ -401,6 +401,11 @@ export function parseCommandLineArguments(args: Array): 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', diff --git a/packages/aws-cdk/test/api/deploy-stack.test.ts b/packages/aws-cdk/test/api/deploy-stack.test.ts index 024b6689c5217..161cfceab1c4d 100644 --- a/packages/aws-cdk/test/api/deploy-stack.test.ts +++ b/packages/aws-cdk/test/api/deploy-stack.test.ts @@ -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'],