Skip to content

feat(cli): add cdk orphan command to detach resources from a stack#1324

Open
rix0rrr wants to merge 1 commit intoaws:mainfrom
LeeroyHannigan:lhnng-orphan-resource
Open

feat(cli): add cdk orphan command to detach resources from a stack#1324
rix0rrr wants to merge 1 commit intoaws:mainfrom
LeeroyHannigan:lhnng-orphan-resource

Conversation

@rix0rrr
Copy link
Copy Markdown
Contributor

@rix0rrr rix0rrr commented Apr 9, 2026

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.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

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.
@rix0rrr
Copy link
Copy Markdown
Contributor Author

rix0rrr commented Apr 9, 2026

This is actually @LeeroyHannigan's change, I'm just turning it into a PR so I can leave comments more conveniently.

@github-actions github-actions bot added the p2 label Apr 9, 2026
@aws-cdk-automation aws-cdk-automation requested a review from a team April 9, 2026 08:14
Copy link
Copy Markdown
Contributor Author

@rix0rrr rix0rrr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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> {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}`));
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
     }
   }
}

Comment on lines +959 to +960
roleArn: options.roleArn,
toolkitStackName: this.toolkitStackName,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 });
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this paginated?

}
}

private replaceInObject(obj: any, logicalId: string, values: { ref: string; attrs: Record<string, string> }): any {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature needs to be --unstable. Check out other code to see how we do that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant