feat(cli): add cdk orphan command to detach resources from a stack#1324
feat(cli): add cdk orphan command to detach resources from a stack#1324
cdk orphan command to detach resources from a stack#1324Conversation
Adds a new CLI command that safely removes resources from a CloudFormation
stack without deleting them, enabling resource type migrations (e.g.
DynamoDB Table to GlobalTable).
`cdk orphan --path <ConstructPath>` will:
- Find all resources under the construct path via aws:cdk:path metadata
- Resolve {Ref} values via DescribeStackResources
- Resolve {Fn::GetAtt} values by injecting temporary stack Outputs
- Set DeletionPolicy: Retain on all matched resources
- Remove the resources from the stack
- Output an inline `cdk import` command with the resource mapping
Also adds inline JSON support for `--resource-mapping` in `cdk import`,
and exposes `stackSdk()` on Deployments for read-only SDK access.
|
This is actually @LeeroyHannigan's change, I'm just turning it into a PR so I can leave comments more conveniently. |
rix0rrr
left a comment
There was a problem hiding this comment.
Great start!
Initial round of comments on this.
This also needs an integ test (and to be frank probably more than 1 😉 ).
| * Get the CloudFormation SDK client for a stack's environment. | ||
| * Used by the orphaner to call DescribeStackResources. | ||
| */ | ||
| public async stackSdk(stackArtifact: cxapi.CloudFormationStackArtifact) { |
There was a problem hiding this comment.
Do we need this method? We probably should just inline this at the call site.
| */ | ||
| public async loadResourceIdentifiers(available: ImportableResource[], filename: string): Promise<ImportMap> { | ||
| const contents = await fs.readJson(filename); | ||
| public async loadResourceIdentifiers(available: ImportableResource[], filenameOrJson: string): Promise<ImportMap> { |
There was a problem hiding this comment.
Not a fan of implicitly overloading an argument like this. I'd rather be explicit, like this:
type ResourceIdentifiersSource =
| { type: 'file'; fileName: string }
| { type: 'direct'; resourceIdentifiers: Record<string, ResourceIdentifierProperties> };
public async loadResourceIdentifiers(available: ImportableResource[], source: ResourceIdentifiersSource): Promise<ImportMap> {Or honestly, even simpler
public async loadResourceIdentifiersFromFile(available: ImportableResource[], fileName: string): Promise<ImportMap> {
const contents = /* load file */;
return this.loadResourceIdentifiers(available, contents);
}
public async loadResourceIdentifiers(available: ImportableResource[], identifiers: Record<string, ResourceIdentifierProperties>): Promise<ImportMap> {| } | ||
|
|
||
| public async orphan(options: OrphanOptions) { | ||
| const stacks = await this.selectStacksForDeploy(options.selector, true, true, false); |
There was a problem hiding this comment.
If the argument is one or more construct paths (which it should be) then the stack selection is implicit. No need for the user to pick the stack.
| } | ||
|
|
||
| const stack = stacks.stackArtifacts[0]; | ||
| await this.ioHost.asIoHelper().defaults.info(chalk.bold(`Orphaning construct '${options.constructPath}' from ${stack.displayName}`)); |
There was a problem hiding this comment.
We should split this into "plan" and "execute" stages:
- First determine actually which constructs are going to be orphaned, by evaluating the construct path. And include what resources are going to have their references replaced with literals. It should probably be an error if there are 0. Then we print a proper report of what we're going to do to the user (not a guess at the result of their instruction). We should probably give them a chance to confirm as well.
- Only after they confirm do we do the actual work.
I would like to see that in the API in some way. For example:
class Orphaner {
constructor(...) { }
public async makePlan(...): Promise<OrphanPlan> {
// ...
}
}
class OrphanPlan {
// The properties below here are purely hypothetical to show the idea! I have not done enough
// mental design to think about whether these are the best to expose.
public readonly orphanedResoures: OrpanPlanResource[];
public readonly affectedResources: OrpanPlanResource[];
public readonly stacks: OrphanPlanStack[];
public async execute() {
// ...
}
}
class Toolkit {
public async orphan(...) {
// And then something like this
const orphaner = new Orphaner(...):
const plan = await orphaner.makePlan(...);
await showPlanToUser(plan, this.ioHost);
const yes = await this.ioHost.confirm(...);
if (yes) {
await plan.execute();
}
}
}| roleArn: options.roleArn, | ||
| toolkitStackName: this.toolkitStackName, |
There was a problem hiding this comment.
Feels like both of these should be constructor arguments.
| const cfn = sdk.cloudFormation(); | ||
|
|
||
| // Get physical resource IDs (Ref values) from CloudFormation | ||
| const describeResult = await cfn.describeStackResources({ StackName: stack.stackName }); |
| } | ||
| } | ||
|
|
||
| private replaceInObject(obj: any, logicalId: string, values: { ref: string; attrs: Record<string, string> }): any { |
There was a problem hiding this comment.
Doesn't use this so doesn't need to be a method. Could be a helper function.
And in fact should be since it operates on a CFN template.
| return result; | ||
| } | ||
|
|
||
| private removeDependsOn(template: any, logicalId: string): void { |
There was a problem hiding this comment.
Doesn't use this so doesn't need to be a method. Could be a helper function.
And in fact should be since it operates on a CFN template.
| } | ||
| } | ||
|
|
||
| private walkObject(obj: any, visitor: (value: any) => void): void { |
There was a problem hiding this comment.
Doesn't use this so doesn't need to be a method. Could be a helper function.
And in fact should be since it operates on a CFN template.
| }); | ||
| } | ||
|
|
||
| public async orphan(options: OrphanOptions) { |
There was a problem hiding this comment.
This feature needs to be --unstable. Check out other code to see how we do that.
Adds a new CLI command that safely removes resources from a CloudFormation stack without deleting them, enabling resource type migrations (e.g. DynamoDB Table to GlobalTable).
cdk orphan --path <ConstructPath>will:cdk importcommand with the resource mappingAlso adds inline JSON support for
--resource-mappingincdk import, and exposesstackSdk()on Deployments for read-only SDK access.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license