Skip to content

Commit 273569d

Browse files
committed
feat(cli): Add an option to import existing resources (currently S3 buckets only)
This is an initial proposal to support existing resources import into CDK stacks. As a PoC, this PR shows a working solution for S3 buckets. This is achieved by introducing `-i` / `--import-resources` CLI option to the `cdk deploy` command. If specified, the newly added resources will not be created, but attempted to be imported (adopted) instead. If the resource definition contains the full resource identifier, this happens automatically. For resources that can't be identified (e.g. an S3 bucket without an explicit `bucketName`), user will be prompted for the necessary information.
1 parent ddf2881 commit 273569d

File tree

8 files changed

+129
-5
lines changed

8 files changed

+129
-5
lines changed

Diff for: packages/aws-cdk/README.md

+27
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,33 @@ to turn them off, pass the `--no-hotswap` option when invoking it.
435435
**Note**: This command is considered experimental,
436436
and might have breaking changes in the future.
437437

438+
#### Import existing resources
439+
440+
**Important:** This is a work in progress, only S3 buckets are currently supported
441+
442+
Sometimes, it is beneficial to import (enroll/adopt/...) AWS resources, that were
443+
created manually (or by different means), into a CDK stack. Some resources can simply be
444+
deleted and recreated by CDK, but for others, this is not convenient: Typically stateful
445+
resources like S3 Buckets, DynamoDB tables, etc., cannot be easily deleted without an
446+
impact on the service.
447+
448+
To import an existing resource to a CDK stack:
449+
450+
* run a `cdk diff` to ensure there are no pending changes to the CDK stack you want to
451+
import resources into - if there are, apply/discard them first
452+
* add corresponding constructs for the resources to be added in your stack - for example,
453+
for an S3 bucket, add something like `new s3.Bucket(this, 'ImportedS3Bucket', {});` -
454+
**no other changes must be done to the stack before the import is completed**
455+
* run `cdk deploy` with `--import-resources` argument to instruct CDK to start the import
456+
operation
457+
* if resource definition contains all information needed for the import, this happens
458+
automatically (e.g. an `s3.Bucket` construct has an explicit `bucketName` set),
459+
otherwise, CDK will prompt user to provide neccessary identification information (e.g.
460+
the bucket name)
461+
* after cdk deploy reports success, the resource is managed by CDK. Any subsequent
462+
changes in the construct configuration will be reflected on the resource
463+
464+
438465
### `cdk destroy`
439466

440467
Deletes a stack from it's environment. This will cause the resources in the stack to be destroyed (unless they were

Diff for: packages/aws-cdk/bin/cdk.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ async function parseCommandLineArguments() {
124124
desc: 'Continuously observe the project files, ' +
125125
'and deploy the given stack(s) automatically when changes are detected. ' +
126126
'Implies --hotswap by default',
127-
}),
127+
})
128+
.option('import-resources', { type: 'boolean', alias: 'i', desc: 'Import existing resources in the stack' }),
128129
)
129130
.command('watch [STACKS..]', "Shortcut for 'deploy --watch'", yargs => yargs
130131
// I'm fairly certain none of these options, present for 'deploy', make sense for 'watch':
@@ -375,6 +376,7 @@ async function initCommandLine() {
375376
rollback: configuration.settings.get(['rollback']),
376377
hotswap: args.hotswap,
377378
watch: args.watch,
379+
importResources: args['import-resources'],
378380
});
379381

380382
case 'watch':

Diff for: packages/aws-cdk/lib/api/cloudformation-deployments.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { publishAssets } from '../util/asset-publishing';
66
import { Mode, SdkProvider } from './aws-auth';
77
import { deployStack, DeployStackResult, destroyStack } from './deploy-stack';
88
import { ToolkitInfo } from './toolkit-info';
9-
import { CloudFormationStack, Template } from './util/cloudformation';
9+
import { CloudFormationStack, Template, ResourcesToImport } from './util/cloudformation';
1010
import { StackActivityProgress } from './util/cloudformation/stack-activity-monitor';
1111

1212
/**
@@ -152,6 +152,11 @@ export interface DeployStackOptions {
152152
* @default - nothing extra is appended to the User-Agent header
153153
*/
154154
readonly extraUserAgent?: string;
155+
156+
/**
157+
* List of existing resources to be IMPORTED into the stack, instead of being CREATED
158+
*/
159+
readonly resourcesToImport?: ResourcesToImport;
155160
}
156161

157162
export interface DestroyStackOptions {
@@ -230,6 +235,7 @@ export class CloudFormationDeployments {
230235
rollback: options.rollback,
231236
hotswap: options.hotswap,
232237
extraUserAgent: options.extraUserAgent,
238+
resourcesToImport: options.resourcesToImport,
233239
});
234240
}
235241

Diff for: packages/aws-cdk/lib/api/deploy-stack.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { CfnEvaluationException } from './hotswap/evaluate-cloudformation-templa
1414
import { ToolkitInfo } from './toolkit-info';
1515
import {
1616
changeSetHasNoChanges, CloudFormationStack, TemplateParameters, waitForChangeSet,
17-
waitForStackDeploy, waitForStackDelete, ParameterValues, ParameterChanges,
17+
waitForStackDeploy, waitForStackDelete, ParameterValues, ParameterChanges, ResourcesToImport,
1818
} from './util/cloudformation';
1919
import { StackActivityMonitor, StackActivityProgress } from './util/cloudformation/stack-activity-monitor';
2020

@@ -189,6 +189,12 @@ export interface DeployStackOptions {
189189
* @default - nothing extra is appended to the User-Agent header
190190
*/
191191
readonly extraUserAgent?: string;
192+
193+
/**
194+
* If set, change set of type IMPORT will be created, and resourcesToImport
195+
* passed to it.
196+
*/
197+
readonly resourcesToImport?: ResourcesToImport;
192198
}
193199

194200
const LARGE_TEMPLATE_SIZE_KB = 50;
@@ -294,7 +300,8 @@ async function prepareAndExecuteChangeSet(
294300
const changeSet = await cfn.createChangeSet({
295301
StackName: deployName,
296302
ChangeSetName: changeSetName,
297-
ChangeSetType: update ? 'UPDATE' : 'CREATE',
303+
ChangeSetType: options.resourcesToImport ? 'IMPORT' : update ? 'UPDATE' : 'CREATE',
304+
ResourcesToImport: options.resourcesToImport,
298305
Description: `CDK Changeset for execution ${executionId}`,
299306
TemplateBody: bodyParameter.TemplateBody,
300307
TemplateURL: bodyParameter.TemplateURL,

Diff for: packages/aws-cdk/lib/api/util/cloudformation.ts

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ interface TemplateParameter {
1616
[key: string]: any;
1717
}
1818

19+
export type ResourcesToImport = CloudFormation.ResourcesToImport;
20+
1921
/**
2022
* Represents an (existing) Stack in CloudFormation
2123
*

Diff for: packages/aws-cdk/lib/api/util/cloudformation/stack-status.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class StackStatus {
3434
}
3535

3636
get isDeploySuccess(): boolean {
37-
return !this.isNotFound && (this.name === 'CREATE_COMPLETE' || this.name === 'UPDATE_COMPLETE');
37+
return !this.isNotFound && (this.name === 'CREATE_COMPLETE' || this.name === 'UPDATE_COMPLETE' || this.name === 'IMPORT_COMPLETE');
3838
}
3939

4040
public toString(): string {

Diff for: packages/aws-cdk/lib/cdk-toolkit.ts

+27
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import { Bootstrapper, BootstrapEnvironmentOptions } from './api/bootstrap';
1111
import { CloudFormationDeployments } from './api/cloudformation-deployments';
1212
import { CloudAssembly, DefaultSelection, ExtendedStackSelection, StackCollection, StackSelector } from './api/cxapp/cloud-assembly';
1313
import { CloudExecutable } from './api/cxapp/cloud-executable';
14+
import { ResourcesToImport } from './api/util/cloudformation';
1415
import { StackActivityProgress } from './api/util/cloudformation/stack-activity-monitor';
1516
import { printSecurityDiff, printStackDiff, RequireApproval } from './diff';
17+
import { prepareResourcesToImport } from './import';
1618
import { data, debug, error, highlight, print, success, warning } from './logging';
1719
import { deserializeStructure } from './serialize';
1820
import { Configuration, PROJECT_CONFIG } from './settings';
@@ -117,6 +119,11 @@ export class CdkToolkit {
117119
return this.watch(options);
118120
}
119121

122+
// TODO - print more intelligent message
123+
if (options.importResources) {
124+
warning('Import resources flag was set');
125+
}
126+
120127
const stacks = await this.selectStacksForDeploy(options.selector, options.exclusively, options.cacheCloudAssembly);
121128

122129
const requireApproval = options.requireApproval ?? RequireApproval.Broadening;
@@ -167,6 +174,17 @@ export class CdkToolkit {
167174
continue;
168175
}
169176

177+
let resourcesToImport: ResourcesToImport | undefined = undefined;
178+
if (options.importResources) {
179+
const currentTemplate = await this.props.cloudFormation.readCurrentTemplate(stack);
180+
resourcesToImport = await prepareResourcesToImport(currentTemplate, stack);
181+
182+
// There's a CloudFormation limitation that on import operation, no other changes are allowed:
183+
// As CDK always changes the CDKMetadata resource with a new value, as a workaround, we override
184+
// the template's metadata with currently deployed version
185+
stack.template.Resources.CDKMetadata = currentTemplate.Resources.CDKMetadata;
186+
}
187+
170188
if (requireApproval !== RequireApproval.Never) {
171189
const currentTemplate = await this.props.cloudFormation.readCurrentTemplate(stack);
172190
if (printSecurityDiff(currentTemplate, stack, requireApproval)) {
@@ -209,6 +227,7 @@ export class CdkToolkit {
209227
rollback: options.rollback,
210228
hotswap: options.hotswap,
211229
extraUserAgent: options.extraUserAgent,
230+
resourcesToImport,
212231
});
213232

214233
const message = result.noOp
@@ -799,6 +818,14 @@ export interface DeployOptions extends WatchOptions {
799818
* @default true
800819
*/
801820
readonly cacheCloudAssembly?: boolean;
821+
822+
/**
823+
* Whether to import matching existing resources for newly defined constructs in the stack,
824+
* rather than creating new ones
825+
*
826+
* @default false
827+
*/
828+
readonly importResources?: boolean;
802829
}
803830

804831
export interface DestroyOptions {

Diff for: packages/aws-cdk/lib/import.ts

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import * as cfnDiff from '@aws-cdk/cloudformation-diff';
2+
import * as cxapi from '@aws-cdk/cx-api';
3+
import * as promptly from 'promptly';
4+
import { ResourcesToImport } from './api/util/cloudformation';
5+
6+
// Basic idea: we want to have a structure (ideally auto-generated from CFN definitions) that lists all resource types
7+
// that support importing and for each type, the identification information
8+
//
9+
// For each resource that is to be added in the new template:
10+
// - look up the identification information for the resource type [if not found, fail "type not supported"]
11+
// - look up the physical resource (perhaps using cloud control API?) [if not found, fail "resource to be imported does not exist"]
12+
// - assembe and return "resources to import" object to be passed on to changeset creation
13+
//
14+
// TEST: can we have a CFN changeset that both creates resources and import other resources?
15+
const RESOURCE_IDENTIFIERS: { [key: string]: string[] } = {
16+
'AWS::S3::Bucket': ['BucketName'],
17+
};
18+
19+
export async function prepareResourcesToImport(oldTemplate: any, newTemplate: cxapi.CloudFormationStackArtifact): Promise<ResourcesToImport> {
20+
const diff = cfnDiff.diffTemplate(oldTemplate, newTemplate.template);
21+
22+
const additions: { [key: string]: cfnDiff.ResourceDifference } = {};
23+
diff.resources.forEachDifference((id, chg) => {
24+
if (chg.isAddition) {
25+
additions[id] = chg;
26+
}
27+
});
28+
29+
const resourcesToImport: ResourcesToImport = [];
30+
for (let [id, chg] of Object.entries(additions)) {
31+
if (chg.newResourceType === undefined || !(chg.newResourceType in RESOURCE_IDENTIFIERS)) {
32+
throw new Error(`Resource ${id} is of type ${chg.newResourceType} that is not supported for import`);
33+
}
34+
35+
let identifier: { [key: string]: string } = {};
36+
for (let idpart of RESOURCE_IDENTIFIERS[chg.newResourceType]) {
37+
if (chg.newProperties && (idpart in chg.newProperties)) {
38+
identifier[idpart] = chg.newProperties[idpart];
39+
} else {
40+
const displayName : string = newTemplate.template?.Resources?.[id]?.Metadata?.['aws:cdk:path'] ?? id;
41+
identifier[idpart] = await promptly.prompt(`Please enter ${idpart} of ${chg.newResourceType} to import as ${displayName.replace(/\/Resource$/, '')}: `);
42+
}
43+
}
44+
45+
resourcesToImport.push({
46+
LogicalResourceId: id,
47+
ResourceType: chg.newResourceType,
48+
ResourceIdentifier: identifier,
49+
});
50+
}
51+
52+
return resourcesToImport;
53+
}

0 commit comments

Comments
 (0)