Skip to content

Commit 811ec58

Browse files
chore(migrate): enable import of resources on apps created from cdk migrate (#28678)
Apps generated from cdk migrate with resources that aren't already part of a stack will (soon) create a migrate.json file. This file contains the list of resources that should be imported upon creation of the new app. If this file is present and the source is either `localfile` or the ARN environment matches the deployment environment, running `cdk deploy` will: 1. Create a new stack and import the resources listed in migrate.json. 2. Apply outputs and CDKMetadata through a normal deployment. Note: `localfile` is a placeholder value so that we can run integration tests on this change. Once some of the other in-progress work is finished, this will be updated. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent f1275a7 commit 811ec58

File tree

6 files changed

+221
-15
lines changed

6 files changed

+221
-15
lines changed

packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class YourStack extends cdk.Stack {
6565
}
6666
}
6767

68-
class ImportableStack extends cdk.Stack {
68+
class MigrateStack extends cdk.Stack {
6969
constructor(parent, id, props) {
7070
super(parent, id, props);
7171

@@ -77,11 +77,22 @@ class ImportableStack extends cdk.Stack {
7777
new cdk.CfnOutput(this, 'QueueName', {
7878
value: queue.queueName,
7979
});
80+
81+
new cdk.CfnOutput(this, 'QueueUrl', {
82+
value: queue.queueUrl,
83+
});
84+
8085
new cdk.CfnOutput(this, 'QueueLogicalId', {
8186
value: queue.node.defaultChild.logicalId,
8287
});
8388
}
8489

90+
}
91+
}
92+
93+
class ImportableStack extends MigrateStack {
94+
constructor(parent, id, props) {
95+
super(parent, id, props);
8596
new cdk.CfnWaitConditionHandle(this, 'Handle');
8697
}
8798
}
@@ -470,6 +481,8 @@ switch (stackSet) {
470481

471482
new ImportableStack(app, `${stackPrefix}-importable-stack`);
472483

484+
new MigrateStack(app, `${stackPrefix}-migrate-stack`);
485+
473486
new ExportValueStack(app, `${stackPrefix}-export-value-stack`);
474487

475488
new BundlingStage(app, `${stackPrefix}-bundling-stage`);

packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts

+39
Original file line numberDiff line numberDiff line change
@@ -1213,6 +1213,45 @@ integTest('test resource import', withDefaultFixture(async (fixture) => {
12131213
}
12141214
}));
12151215

1216+
integTest('test migrate deployment for app with localfile source in migrate.json', withDefaultFixture(async (fixture) => {
1217+
const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json');
1218+
await fs.mkdir(path.dirname(outputsFile), { recursive: true });
1219+
1220+
// Initial deploy
1221+
await fixture.cdkDeploy('migrate-stack', {
1222+
modEnv: { ORPHAN_TOPIC: '1' },
1223+
options: ['--outputs-file', outputsFile],
1224+
});
1225+
1226+
const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString());
1227+
const stackName = fixture.fullStackName('migrate-stack');
1228+
const queueName = outputs[stackName].QueueName;
1229+
const queueUrl = outputs[stackName].QueueUrl;
1230+
const queueLogicalId = outputs[stackName].QueueLogicalId;
1231+
fixture.log(`Created queue ${queueUrl} in stack ${fixture.fullStackName}`);
1232+
1233+
// Write the migrate file based on the ID from step one, then deploy the app with migrate
1234+
const migrateFile = path.join(fixture.integTestDir, 'migrate.json');
1235+
await fs.writeFile(
1236+
migrateFile, JSON.stringify(
1237+
{ Source: 'localfile', Resources: [{ ResourceType: 'AWS::SQS::Queue', LogicalResourceId: queueLogicalId, ResourceIdentifier: { QueueUrl: queueUrl } }] },
1238+
),
1239+
{ encoding: 'utf-8' },
1240+
);
1241+
1242+
await fixture.cdkDestroy('migrate-stack');
1243+
fixture.log(`Deleted stack ${fixture.fullStackName}, orphaning ${queueName}`);
1244+
1245+
// Create new stack from existing queue
1246+
try {
1247+
fixture.log(`Deploying new stack ${fixture.fullStackName}, migrating ${queueName} into stack`);
1248+
await fixture.cdkDeploy('migrate-stack');
1249+
} finally {
1250+
// Cleanup
1251+
await fixture.cdkDestroy('migrate-stack');
1252+
}
1253+
}));
1254+
12161255
integTest('hotswap deployment supports Lambda function\'s description and environment variables', withDefaultFixture(async (fixture) => {
12171256
// GIVEN
12181257
const stackArn = await fixture.cdkDeploy('lambda-hotswap', {

packages/aws-cdk/lib/api/deployments.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,13 @@ export class Deployments {
317317
this.environmentResources = new EnvironmentResourcesRegistry(props.toolkitStackName);
318318
}
319319

320+
/**
321+
* Resolves the environment for a stack.
322+
*/
323+
public async resolveEnvironment(stack: cxapi.CloudFormationStackArtifact): Promise<cxapi.Environment> {
324+
return this.sdkProvider.resolveEnvironment(stack.environment);
325+
}
326+
320327
public async readCurrentTemplateWithNestedStacks(
321328
rootStackArtifact: cxapi.CloudFormationStackArtifact,
322329
retrieveProcessedTemplate: boolean = false,
@@ -470,7 +477,7 @@ export class Deployments {
470477
throw new Error(`The stack ${stack.displayName} does not have an environment`);
471478
}
472479

473-
const resolvedEnvironment = await this.sdkProvider.resolveEnvironment(stack.environment);
480+
const resolvedEnvironment = await this.resolveEnvironment(stack);
474481

475482
// Substitute any placeholders with information about the current environment
476483
const arns = await replaceEnvPlaceholders({

packages/aws-cdk/lib/cdk-toolkit.ts

+61-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { Deployments } from './api/deployments';
1515
import { HotswapMode } from './api/hotswap/common';
1616
import { findCloudWatchLogGroups } from './api/logs/find-cloudwatch-logs';
1717
import { CloudWatchLogEventMonitor } from './api/logs/logs-monitor';
18-
import { createDiffChangeSet } from './api/util/cloudformation';
18+
import { createDiffChangeSet, ResourcesToImport } from './api/util/cloudformation';
1919
import { StackActivityProgress } from './api/util/cloudformation/stack-activity-monitor';
2020
import { generateCdkApp, generateStack, readFromPath, readFromStack, setEnvironment, validateSourceOptions } from './commands/migrate';
2121
import { printSecurityDiff, printStackDiff, RequireApproval } from './diff';
@@ -205,6 +205,8 @@ export class CdkToolkit {
205205
const elapsedSynthTime = new Date().getTime() - startSynthTime;
206206
print('\n✨ Synthesis time: %ss\n', formatTime(elapsedSynthTime));
207207

208+
await this.tryMigrateResources(stackCollection, options);
209+
208210
const requireApproval = options.requireApproval ?? RequireApproval.Broadening;
209211

210212
const parameterMap = buildParameterMap(options.parameters);
@@ -539,9 +541,7 @@ export class CdkToolkit {
539541
// Import the resources according to the given mapping
540542
print('%s: importing resources into stack...', chalk.bold(stack.displayName));
541543
const tags = tagsForStack(stack);
542-
await resourceImporter.importResources(actualImport, {
543-
stack,
544-
deployName: stack.stackName,
544+
await resourceImporter.importResourcesFromMap(actualImport, {
545545
roleArn: options.roleArn,
546546
toolkitStackName: options.toolkitStackName,
547547
tags,
@@ -874,6 +874,63 @@ export class CdkToolkit {
874874
stackName: assetNode.parentStack.stackName,
875875
}));
876876
}
877+
878+
/**
879+
* Checks to see if a migrate.json file exists. If it does and the source is either `filepath` or
880+
* is in the same environment as the stack deployment, a new stack is created and the resources are
881+
* migrated to the stack using an IMPORT changeset. The normal deployment will resume after this is complete
882+
* to add back in any outputs and the CDKMetadata.
883+
*/
884+
private async tryMigrateResources(stacks: StackCollection, options: DeployOptions): Promise<void> {
885+
const stack = stacks.stackArtifacts[0];
886+
const migrateDeployment = new ResourceImporter(stack, this.props.deployments);
887+
const resourcesToImport = await this.tryGetResources(migrateDeployment);
888+
889+
if (resourcesToImport) {
890+
print('%s: creating stack for resource migration...', chalk.bold(stack.displayName));
891+
print('%s: importing resources into stack...', chalk.bold(stack.displayName));
892+
893+
await this.performResourceMigration(migrateDeployment, resourcesToImport, options);
894+
895+
fs.rmSync('migrate.json');
896+
print('%s: applying CDKMetadata and Outputs to stack (if applicable)...', chalk.bold(stack.displayName));
897+
}
898+
}
899+
900+
/**
901+
* Creates a new stack with just the resources to be migrated
902+
*/
903+
private async performResourceMigration(migrateDeployment: ResourceImporter, resourcesToImport: ResourcesToImport, options: DeployOptions) {
904+
const startDeployTime = new Date().getTime();
905+
let elapsedDeployTime = 0;
906+
907+
// Initial Deployment
908+
await migrateDeployment.importResourcesFromMigrate(resourcesToImport, {
909+
roleArn: options.roleArn,
910+
toolkitStackName: options.toolkitStackName,
911+
deploymentMethod: options.deploymentMethod,
912+
usePreviousParameters: true,
913+
progress: options.progress,
914+
rollback: options.rollback,
915+
});
916+
917+
elapsedDeployTime = new Date().getTime() - startDeployTime;
918+
print('\n✨ Resource migration time: %ss\n', formatTime(elapsedDeployTime));
919+
}
920+
921+
private async tryGetResources(migrateDeployment: ResourceImporter) {
922+
try {
923+
const migrateFile = fs.readJsonSync('migrate.json', { encoding: 'utf-8' });
924+
const sourceEnv = (migrateFile.Source as string).split(':');
925+
const environment = await migrateDeployment.resolveEnvironment();
926+
if (sourceEnv[0] === 'localfile' ||
927+
(sourceEnv[4] === environment.account && sourceEnv[3] === environment.region)) {
928+
return migrateFile.Resources;
929+
}
930+
} catch (e) {
931+
// Nothing to do
932+
}
933+
}
877934
}
878935

879936
export interface DiffOptions {

packages/aws-cdk/lib/import.ts

+53-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
1+
import { DeployOptions } from '@aws-cdk/cloud-assembly-schema';
12
import * as cfnDiff from '@aws-cdk/cloudformation-diff';
23
import { ResourceDifference } from '@aws-cdk/cloudformation-diff';
34
import * as cxapi from '@aws-cdk/cx-api';
45
import * as chalk from 'chalk';
56
import * as fs from 'fs-extra';
67
import * as promptly from 'promptly';
7-
import { Deployments, DeployStackOptions } from './api/deployments';
8+
import { DeploymentMethod } from './api';
9+
import { Deployments } from './api/deployments';
810
import { ResourceIdentifierProperties, ResourcesToImport } from './api/util/cloudformation';
11+
import { StackActivityProgress } from './api/util/cloudformation/stack-activity-monitor';
12+
import { Tag } from './cdk-toolkit';
913
import { error, print, success, warning } from './logging';
1014

15+
export interface ImportDeploymentOptions extends DeployOptions {
16+
deploymentMethod?: DeploymentMethod;
17+
progress?: StackActivityProgress;
18+
tags?: Tag[];
19+
}
20+
1121
/**
1222
* Set of parameters that uniquely identify a physical resource of a given type
1323
* for the import operation, example:
@@ -112,24 +122,44 @@ export class ResourceImporter {
112122
* @param importMap Mapping from CDK construct tree path to physical resource import identifiers
113123
* @param options Options to pass to CloudFormation deploy operation
114124
*/
115-
public async importResources(importMap: ImportMap, options: DeployStackOptions) {
125+
public async importResourcesFromMap(importMap: ImportMap, options: ImportDeploymentOptions) {
116126
const resourcesToImport: ResourcesToImport = await this.makeResourcesToImport(importMap);
117127
const updatedTemplate = await this.currentTemplateWithAdditions(importMap.importResources);
118128

129+
await this.importResources(updatedTemplate, resourcesToImport, options);
130+
}
131+
132+
/**
133+
* Based on the app and resources file generated by cdk migrate. Removes all items from the template that
134+
* cannot be included in an import change-set for new stacks and performs the import operation,
135+
* creating the new stack.
136+
*
137+
* @param resourcesToImport The mapping created by cdk migrate
138+
* @param options Options to pass to CloudFormation deploy operation
139+
*/
140+
public async importResourcesFromMigrate(resourcesToImport: ResourcesToImport, options: ImportDeploymentOptions) {
141+
const updatedTemplate = this.removeNonImportResources();
142+
143+
await this.importResources(updatedTemplate, resourcesToImport, options);
144+
}
145+
146+
private async importResources(overrideTemplate: any, resourcesToImport: ResourcesToImport, options: ImportDeploymentOptions) {
119147
try {
120148
const result = await this.cfn.deployStack({
149+
stack: this.stack,
150+
deployName: this.stack.stackName,
121151
...options,
122-
overrideTemplate: updatedTemplate,
152+
overrideTemplate,
123153
resourcesToImport,
124154
});
125155

126156
const message = result.noOp
127157
? ' ✅ %s (no changes)'
128158
: ' ✅ %s';
129159

130-
success('\n' + message, options.stack.displayName);
160+
success('\n' + message, this.stack.displayName);
131161
} catch (e) {
132-
error('\n ❌ %s failed: %s', chalk.bold(options.stack.displayName), e);
162+
error('\n ❌ %s failed: %s', chalk.bold(this.stack.displayName), e);
133163
throw e;
134164
}
135165
}
@@ -176,6 +206,13 @@ export class ResourceImporter {
176206
};
177207
}
178208

209+
/**
210+
* Resolves the environment of a stack.
211+
*/
212+
public async resolveEnvironment(): Promise<cxapi.Environment> {
213+
return this.cfn.resolveEnvironment(this.stack);
214+
}
215+
179216
/**
180217
* Get currently deployed template of the given stack (SINGLETON)
181218
*
@@ -342,6 +379,17 @@ export class ResourceImporter {
342379
private describeResource(logicalId: string): string {
343380
return this.stack.template?.Resources?.[logicalId]?.Metadata?.['aws:cdk:path'] ?? logicalId;
344381
}
382+
383+
/**
384+
* Removes CDKMetadata and Outputs in the template so that only resources for importing are left.
385+
* @returns template with import resources only
386+
*/
387+
private removeNonImportResources() {
388+
const template = this.stack.template;
389+
delete template.Resources.CDKMetadata;
390+
delete template.Outputs;
391+
return template;
392+
}
345393
}
346394

347395
/**

packages/aws-cdk/test/import.test.ts

+46-4
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,7 @@ test('asks human to confirm automic import if identifier is in template', async
164164
};
165165

166166
// WHEN
167-
await importer.importResources(importMap, {
168-
stack: STACK_WITH_QUEUE,
169-
});
167+
await importer.importResourcesFromMap(importMap, {});
170168

171169
expect(createChangeSetInput?.ResourcesToImport).toEqual([
172170
{
@@ -177,6 +175,50 @@ test('asks human to confirm automic import if identifier is in template', async
177175
]);
178176
});
179177

178+
test('importing resources from migrate strips cdk metadata and outputs', async () => {
179+
// GIVEN
180+
181+
const MyQueue = {
182+
Type: 'AWS::SQS::Queue',
183+
Properties: {},
184+
};
185+
const stack = {
186+
stackName: 'StackWithQueue',
187+
template: {
188+
Resources: {
189+
MyQueue,
190+
CDKMetadata: {
191+
Type: 'AWS::CDK::Metadata',
192+
Properties: {
193+
Analytics: 'exists',
194+
},
195+
},
196+
},
197+
Outputs: {
198+
Output: {
199+
Description: 'There is an output',
200+
Value: 'OutputValue',
201+
},
202+
},
203+
},
204+
};
205+
206+
givenCurrentStack(stack.stackName, stack);
207+
const importer = new ResourceImporter(testStack(stack), deployments);
208+
const migrateMap = [{
209+
LogicalResourceId: 'MyQueue',
210+
ResourceIdentifier: { QueueName: 'TheQueueName' },
211+
ResourceType: 'AWS::SQS::Queue',
212+
}];
213+
214+
// WHEN
215+
await importer.importResourcesFromMigrate(migrateMap, STACK_WITH_QUEUE.template);
216+
217+
// THEN
218+
expect(createChangeSetInput?.ResourcesToImport).toEqual(migrateMap);
219+
expect(createChangeSetInput?.TemplateBody).toEqual('Resources:\n MyQueue:\n Type: AWS::SQS::Queue\n Properties: {}\n');
220+
});
221+
180222
test('only use one identifier if multiple are in template', async () => {
181223
// GIVEN
182224
const stack = stackWithGlobalTable({
@@ -289,7 +331,7 @@ async function importTemplateFromClean(stack: ReturnType<typeof testStack>) {
289331
const importer = new ResourceImporter(stack, deployments);
290332
const { additions } = await importer.discoverImportableResources();
291333
const importable = await importer.askForResourceIdentifiers(additions);
292-
await importer.importResources(importable, { stack });
334+
await importer.importResourcesFromMap(importable, {});
293335
return importable;
294336
}
295337

0 commit comments

Comments
 (0)