Skip to content

Commit 4f12209

Browse files
authored
feat(cli): preview of cdk import (#17666)
An initial version of `cdk import`, bringing existing resources under the management of CloudFormation. To use: - Make sure your diff is clean (you've recently deployed) - Add constructs for the resource(s) you want to import. **Make sure the CDK code configures them exactly as they are configured in reality**. - You can provide resource names here but it's probably better if you don't. - Run `cdk import` - Provide the actual resource names for each resource (if necessary). - An importing changeset will execute and the resources are imported. This is an implementation of aws/aws-cdk-rfcs#52
1 parent 1b4d010 commit 4f12209

18 files changed

+1035
-69
lines changed

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

+58-10
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Command | Description
1919
[`cdk synth`](#cdk-synthesize) | Synthesize a CDK app to CloudFormation template(s)
2020
[`cdk diff`](#cdk-diff) | Diff stacks against current state
2121
[`cdk deploy`](#cdk-deploy) | Deploy a stack into an AWS account
22+
[`cdk import`](#cdk-import) | Import existing AWS resources into a CDK stack
2223
[`cdk watch`](#cdk-watch) | Watches a CDK app for deployable and hotswappable changes
2324
[`cdk destroy`](#cdk-destroy) | Deletes a stack from an AWS account
2425
[`cdk bootstrap`](#cdk-bootstrap) | Deploy a toolkit stack to support deploying large stacks & artifacts
@@ -450,6 +451,53 @@ $ cdk watch --no-logs
450451
**Note**: This command is considered experimental,
451452
and might have breaking changes in the future.
452453

454+
### `cdk import`
455+
456+
Sometimes you want to import AWS resources that were created using other means
457+
into a CDK stack. For some resources (like Roles, Lambda Functions, Event Rules,
458+
...), it's feasible to create new versions in CDK and then delete the old
459+
versions. For other resources, this is not possible: stateful resources like S3
460+
Buckets, DynamoDB tables, etc., cannot be easily deleted without impact on the
461+
service.
462+
463+
`cdk import`, which uses [CloudFormation resource
464+
imports](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import.html),
465+
makes it possible to bring an existing resource under CDK/CloudFormation's
466+
management. See the [list of resources that can be imported here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import-supported-resources.html).
467+
468+
To import an existing resource to a CDK stack, follow the following steps:
469+
470+
1. Run a `cdk diff` to make sure there are no pending changes to the CDK stack you want to
471+
import resources into. The only changes allowed in an "import" operation are
472+
the addition of new resources which you want to import.
473+
2. Add constructs for the resources you want to import to your Stack (for example,
474+
for an S3 bucket, add something like `new s3.Bucket(this, 'ImportedS3Bucket', {});`).
475+
**Do not add any other changes!** You must also make sure to exactly model the state
476+
that the resource currently has. For the example of the Bucket, be sure to
477+
include KMS keys, life cycle policies, and anything else that's relevant
478+
about the bucket. If you do not, subsequent update operations may not do what
479+
you expect.
480+
3. Run the `cdk import` - if there are multiple stacks in the CDK app, pass a specific
481+
stack name as an argument.
482+
4. The CLI will prompt you to pass in the actual names of the resources you are
483+
importing. After you supply it, the import starts.
484+
5. When `cdk import` reports success, the resource is managed by CDK. Any subsequent
485+
changes in the construct configuration will be reflected on the resource.
486+
487+
#### Limitations
488+
489+
This feature is currently in preview. Be aware of the following limitations:
490+
491+
- Importing resources in nested stacks is not possible.
492+
- Uses the deploy role credentials (necessary to read the encrypted staging
493+
bucket). Requires a new version (version 12) of the bootstrap stack, for the added
494+
IAM permissions to the `deploy-role`.
495+
- There is no check on whether the properties you specify are correct and complete
496+
for the imported resource. Try starting a drift detection operation after importing.
497+
- Resources that depend on other resources must all be imported together, or one-by-one
498+
in the right order. The CLI will not help you import dependent resources in the right
499+
order, the CloudFormation deployment will fail with unresolved references.
500+
453501
### `cdk destroy`
454502

455503
Deletes a stack from it's environment. This will cause the resources in the stack to be destroyed (unless they were
@@ -521,10 +569,10 @@ NOTICES
521569
16603 Toggling off auto_delete_objects for Bucket empties the bucket
522570

523571
Overview: If a stack is deployed with an S3 bucket with
524-
auto_delete_objects=True, and then re-deployed with
525-
auto_delete_objects=False, all the objects in the bucket
572+
auto_delete_objects=True, and then re-deployed with
573+
auto_delete_objects=False, all the objects in the bucket
526574
will be deleted.
527-
575+
528576
Affected versions: <1.126.0.
529577

530578
More information at: https://github.com/aws/aws-cdk/issues/16603
@@ -533,12 +581,12 @@ NOTICES
533581
17061 Error when building EKS cluster with monocdk import
534582

535583
Overview: When using monocdk/aws-eks to build a stack containing
536-
an EKS cluster, error is thrown about missing
584+
an EKS cluster, error is thrown about missing
537585
lambda-layer-node-proxy-agent/layer/package.json.
538-
586+
539587
Affected versions: >=1.126.0 <=1.130.0.
540588

541-
More information at: https://github.com/aws/aws-cdk/issues/17061
589+
More information at: https://github.com/aws/aws-cdk/issues/17061
542590

543591

544592
If you don’t want to see an notice anymore, use "cdk acknowledge ID". For example, "cdk acknowledge 16603".
@@ -580,7 +628,7 @@ $cdk acknowledge 16603
580628
### `cdk notices`
581629

582630
List the notices that are relevant to the current CDK repository, regardless of context flags or notices that
583-
have been acknowledged:
631+
have been acknowledged:
584632

585633
```console
586634
$ cdk notices
@@ -589,9 +637,9 @@ NOTICES
589637

590638
16603 Toggling off auto_delete_objects for Bucket empties the bucket
591639

592-
Overview: if a stack is deployed with an S3 bucket with
593-
auto_delete_objects=True, and then re-deployed with
594-
auto_delete_objects=False, all the objects in the bucket
640+
Overview: if a stack is deployed with an S3 bucket with
641+
auto_delete_objects=True, and then re-deployed with
642+
auto_delete_objects=False, all the objects in the bucket
595643
will be deleted.
596644

597645
Affected versions: framework: <=2.15.0 >=2.10.0

Diff for: packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml

+3-1
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,8 @@ Resources:
453453
- cloudformation:DeleteStack
454454
- cloudformation:UpdateTerminationProtection
455455
- sts:GetCallerIdentity
456+
# `cdk import`
457+
- cloudformation:GetTemplateSummary
456458
Resource: "*"
457459
Effect: Allow
458460
- Sid: CliStagingBucket
@@ -507,7 +509,7 @@ Resources:
507509
Type: String
508510
Name:
509511
Fn::Sub: '/cdk-bootstrap/${Qualifier}/version'
510-
Value: '11'
512+
Value: '12'
511513
Outputs:
512514
BucketName:
513515
Description: The name of the S3 bucket owned by the CDK toolkit stack

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

+55-9
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { publishAssets } from '../util/asset-publishing';
66
import { Mode } from './aws-auth/credentials';
77
import { ISDK } from './aws-auth/sdk';
88
import { SdkProvider } from './aws-auth/sdk-provider';
9-
import { deployStack, DeployStackResult, destroyStack } from './deploy-stack';
9+
import { deployStack, DeployStackResult, destroyStack, makeBodyParameterAndUpload } from './deploy-stack';
1010
import { loadCurrentTemplateWithNestedStacks, loadCurrentTemplate } from './nested-stack-helpers';
1111
import { ToolkitInfo } from './toolkit-info';
12-
import { CloudFormationStack, Template } from './util/cloudformation';
12+
import { CloudFormationStack, Template, ResourcesToImport, ResourceIdentifierSummaries } from './util/cloudformation';
1313
import { StackActivityProgress } from './util/cloudformation/stack-activity-monitor';
1414
import { replaceEnvPlaceholders } from './util/placeholders';
1515

@@ -224,6 +224,18 @@ export interface DeployStackOptions {
224224
* @default - nothing extra is appended to the User-Agent header
225225
*/
226226
readonly extraUserAgent?: string;
227+
228+
/**
229+
* List of existing resources to be IMPORTED into the stack, instead of being CREATED
230+
*/
231+
readonly resourcesToImport?: ResourcesToImport;
232+
233+
/**
234+
* If present, use this given template instead of the stored one
235+
*
236+
* @default - Use the stored template
237+
*/
238+
readonly overrideTemplate?: any;
227239
}
228240

229241
export interface DestroyStackOptions {
@@ -280,23 +292,52 @@ export class CloudFormationDeployments {
280292
}
281293

282294
public async readCurrentTemplateWithNestedStacks(rootStackArtifact: cxapi.CloudFormationStackArtifact): Promise<Template> {
283-
const sdk = await this.prepareSdkWithLookupOrDeployRole(rootStackArtifact);
295+
const sdk = (await this.prepareSdkWithLookupOrDeployRole(rootStackArtifact)).stackSdk;
284296
return (await loadCurrentTemplateWithNestedStacks(rootStackArtifact, sdk)).deployedTemplate;
285297
}
286298

287299
public async readCurrentTemplate(stackArtifact: cxapi.CloudFormationStackArtifact): Promise<Template> {
288300
debug(`Reading existing template for stack ${stackArtifact.displayName}.`);
289-
const sdk = await this.prepareSdkWithLookupOrDeployRole(stackArtifact);
301+
const sdk = (await this.prepareSdkWithLookupOrDeployRole(stackArtifact)).stackSdk;
290302
return loadCurrentTemplate(stackArtifact, sdk);
291303
}
292304

305+
public async resourceIdentifierSummaries(
306+
stackArtifact: cxapi.CloudFormationStackArtifact,
307+
toolkitStackName?: string,
308+
): Promise<ResourceIdentifierSummaries> {
309+
debug(`Retrieving template summary for stack ${stackArtifact.displayName}.`);
310+
// Currently, needs to use `deploy-role` since it may need to read templates in the staging
311+
// bucket which have been encrypted with a KMS key (and lookup-role may not read encrypted things)
312+
const { stackSdk, resolvedEnvironment } = await this.prepareSdkFor(stackArtifact, undefined, Mode.ForReading);
313+
const cfn = stackSdk.cloudFormation();
314+
315+
const toolkitInfo = await ToolkitInfo.lookup(resolvedEnvironment, stackSdk, toolkitStackName);
316+
317+
// Upload the template, if necessary, before passing it to CFN
318+
const cfnParam = await makeBodyParameterAndUpload(
319+
stackArtifact,
320+
resolvedEnvironment,
321+
toolkitInfo,
322+
this.sdkProvider,
323+
stackSdk);
324+
325+
const response = await cfn.getTemplateSummary(cfnParam).promise();
326+
if (!response.ResourceIdentifierSummaries) {
327+
debug('GetTemplateSummary API call did not return "ResourceIdentifierSummaries"');
328+
}
329+
return response.ResourceIdentifierSummaries ?? [];
330+
}
331+
293332
public async deployStack(options: DeployStackOptions): Promise<DeployStackResult> {
294333
const { stackSdk, resolvedEnvironment, cloudFormationRoleArn } = await this.prepareSdkFor(options.stack, options.roleArn);
295334

296335
const toolkitInfo = await ToolkitInfo.lookup(resolvedEnvironment, stackSdk, options.toolkitStackName);
297336

298-
// Publish any assets before doing the actual deploy
299-
await this.publishStackAssets(options.stack, toolkitInfo);
337+
// Publish any assets before doing the actual deploy (do not publish any assets on import operation)
338+
if (options.resourcesToImport === undefined) {
339+
await this.publishStackAssets(options.stack, toolkitInfo);
340+
}
300341

301342
// Do a verification of the bootstrap stack version
302343
await this.validateBootstrapStackVersion(
@@ -327,6 +368,8 @@ export class CloudFormationDeployments {
327368
rollback: options.rollback,
328369
hotswap: options.hotswap,
329370
extraUserAgent: options.extraUserAgent,
371+
resourcesToImport: options.resourcesToImport,
372+
overrideTemplate: options.overrideTemplate,
330373
});
331374
}
332375

@@ -348,16 +391,19 @@ export class CloudFormationDeployments {
348391
return stack.exists;
349392
}
350393

351-
private async prepareSdkWithLookupOrDeployRole(stackArtifact: cxapi.CloudFormationStackArtifact): Promise<ISDK> {
394+
private async prepareSdkWithLookupOrDeployRole(stackArtifact: cxapi.CloudFormationStackArtifact): Promise<PreparedSdkForEnvironment> {
352395
// try to assume the lookup role
353396
try {
354397
const result = await prepareSdkWithLookupRoleFor(this.sdkProvider, stackArtifact);
355398
if (result.didAssumeRole) {
356-
return result.sdk;
399+
return {
400+
resolvedEnvironment: result.resolvedEnvironment,
401+
stackSdk: result.sdk,
402+
};
357403
}
358404
} catch { }
359405
// fall back to the deploy role
360-
return (await this.prepareSdkFor(stackArtifact, undefined, Mode.ForReading)).stackSdk;
406+
return this.prepareSdkFor(stackArtifact, undefined, Mode.ForReading);
361407
}
362408

363409
/**

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

+64-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as cxapi from '@aws-cdk/cx-api';
22
import * as chalk from 'chalk';
3+
import * as fs from 'fs-extra';
34
import * as uuid from 'uuid';
45
import { addMetadataAssetsToManifest } from '../assets';
56
import { Tag } from '../cdk-toolkit';
@@ -15,7 +16,7 @@ import { ICON } from './hotswap/common';
1516
import { ToolkitInfo } from './toolkit-info';
1617
import {
1718
changeSetHasNoChanges, CloudFormationStack, TemplateParameters, waitForChangeSet,
18-
waitForStackDeploy, waitForStackDelete, ParameterValues, ParameterChanges,
19+
waitForStackDeploy, waitForStackDelete, ParameterValues, ParameterChanges, ResourcesToImport,
1920
} from './util/cloudformation';
2021
import { StackActivityMonitor, StackActivityProgress } from './util/cloudformation/stack-activity-monitor';
2122

@@ -189,6 +190,19 @@ export interface DeployStackOptions {
189190
* @default - nothing extra is appended to the User-Agent header
190191
*/
191192
readonly extraUserAgent?: string;
193+
194+
/**
195+
* If set, change set of type IMPORT will be created, and resourcesToImport
196+
* passed to it.
197+
*/
198+
readonly resourcesToImport?: ResourcesToImport;
199+
200+
/**
201+
* If present, use this given template instead of the stored one
202+
*
203+
* @default - Use the stored template
204+
*/
205+
readonly overrideTemplate?: any;
192206
}
193207

194208
const LARGE_TEMPLATE_SIZE_KB = 50;
@@ -245,7 +259,13 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt
245259
debug(`${deployName}: deploying...`);
246260
}
247261

248-
const bodyParameter = await makeBodyParameter(stackArtifact, options.resolvedEnvironment, legacyAssets, options.toolkitInfo, options.sdk);
262+
const bodyParameter = await makeBodyParameter(
263+
stackArtifact,
264+
options.resolvedEnvironment,
265+
legacyAssets,
266+
options.toolkitInfo,
267+
options.sdk,
268+
options.overrideTemplate);
249269
await publishAssets(legacyAssets.toManifest(stackArtifact.assembly.directory), options.sdkProvider, stackEnv);
250270

251271
if (options.hotswap) {
@@ -298,7 +318,8 @@ async function prepareAndExecuteChangeSet(
298318
const changeSet = await cfn.createChangeSet({
299319
StackName: deployName,
300320
ChangeSetName: changeSetName,
301-
ChangeSetType: update ? 'UPDATE' : 'CREATE',
321+
ChangeSetType: options.resourcesToImport ? 'IMPORT' : update ? 'UPDATE' : 'CREATE',
322+
ResourcesToImport: options.resourcesToImport,
302323
Description: `CDK Changeset for execution ${executionId}`,
303324
TemplateBody: bodyParameter.TemplateBody,
304325
TemplateURL: bodyParameter.TemplateURL,
@@ -386,15 +407,17 @@ async function makeBodyParameter(
386407
resolvedEnvironment: cxapi.Environment,
387408
assetManifest: AssetManifestBuilder,
388409
toolkitInfo: ToolkitInfo,
389-
sdk: ISDK): Promise<TemplateBodyParameter> {
410+
sdk: ISDK,
411+
overrideTemplate?: any,
412+
): Promise<TemplateBodyParameter> {
390413

391414
// If the template has already been uploaded to S3, just use it from there.
392-
if (stack.stackTemplateAssetObjectUrl) {
415+
if (stack.stackTemplateAssetObjectUrl && !overrideTemplate) {
393416
return { TemplateURL: restUrlFromManifest(stack.stackTemplateAssetObjectUrl, resolvedEnvironment, sdk) };
394417
}
395418

396419
// Otherwise, pass via API call (if small) or upload here (if large)
397-
const templateJson = toYAML(stack.template);
420+
const templateJson = toYAML(overrideTemplate ?? stack.template);
398421

399422
if (templateJson.length <= LARGE_TEMPLATE_SIZE_KB * 1024) {
400423
return { TemplateBody: templateJson };
@@ -413,8 +436,15 @@ async function makeBodyParameter(
413436
const templateHash = contentHash(templateJson);
414437
const key = `cdk/${stack.id}/${templateHash}.yml`;
415438

439+
let templateFile = stack.templateFile;
440+
if (overrideTemplate) {
441+
// Add a variant of this template
442+
templateFile = `${stack.templateFile}-${templateHash}.yaml`;
443+
await fs.writeFile(templateFile, templateJson, { encoding: 'utf-8' });
444+
}
445+
416446
assetManifest.addFileAsset(templateHash, {
417-
path: stack.templateFile,
447+
path: templateFile,
418448
}, {
419449
bucketName: toolkitInfo.bucketName,
420450
objectKey: key,
@@ -425,6 +455,33 @@ async function makeBodyParameter(
425455
return { TemplateURL: templateURL };
426456
}
427457

458+
/**
459+
* Prepare a body parameter for CFN, performing the upload
460+
*
461+
* Return it as-is if it is small enough to pass in the API call,
462+
* upload to S3 and return the coordinates if it is not.
463+
*/
464+
export async function makeBodyParameterAndUpload(
465+
stack: cxapi.CloudFormationStackArtifact,
466+
resolvedEnvironment: cxapi.Environment,
467+
toolkitInfo: ToolkitInfo,
468+
sdkProvider: SdkProvider,
469+
sdk: ISDK,
470+
overrideTemplate?: any): Promise<TemplateBodyParameter> {
471+
472+
// We don't have access to the actual asset manifest here, so pretend that the
473+
// stack doesn't have a pre-published URL.
474+
const forceUploadStack = Object.create(stack, {
475+
stackTemplateAssetObjectUrl: { value: undefined },
476+
});
477+
478+
const builder = new AssetManifestBuilder();
479+
const bodyparam = await makeBodyParameter(forceUploadStack, resolvedEnvironment, builder, toolkitInfo, sdk, overrideTemplate);
480+
const manifest = builder.toManifest(stack.assembly.directory);
481+
await publishAssets(manifest, sdkProvider, resolvedEnvironment, { quiet: true });
482+
return bodyparam;
483+
}
484+
428485
export interface DestroyStackOptions {
429486
/**
430487
* The stack to be destroyed

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

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

19+
export type ResourceIdentifierProperties = CloudFormation.ResourceIdentifierProperties;
20+
export type ResourceIdentifierSummaries = CloudFormation.ResourceIdentifierSummaries;
21+
export type ResourcesToImport = CloudFormation.ResourcesToImport;
22+
1923
/**
2024
* Represents an (existing) Stack in CloudFormation
2125
*

0 commit comments

Comments
 (0)