skip to content

An AWS CDK pull request renames a construct id from 'Table' to 'MainTable', and cdk diff now shows the live DynamoDB table being destroyed and a new one created. Explain how the CDK derives logical IDs from the construct tree, and how you would make that rename safe.

level: seniorimportance: should knowfreq 52%

answer

  1. ids are part of the deployed contract
  2. the path, hashed, becomes the logical ID
  3. CloudFormation has no rename operation
  4. override the logical ID to pin it
  5. retain stateful resources first

basics

~20 s

The CDK derives each resource's CloudFormation logical ID from the construct's path in the tree — the chain of ids from the stack down — plus a hash of that path. Renaming a construct id changes the path, so CloudFormation sees a different resource and replaces it.

solid answer

~60 s

Every construct is created with a scope and an id, and those ids chain into a path. At synth time the CDK turns that path into the CloudFormation logical ID: the human-readable id components concatenated, with a short hash of the full path appended to keep it unique. CloudFormation tracks resources by logical ID and nothing else — it has no notion of "the same table, renamed". So changing the id, or moving the construct under a different parent, produces a new logical ID, and the deploy is read as "delete this resource, create that one". For a stateful resource that is data loss. The safe route is to treat construct ids as part of the deployed contract and simply not rename them — rename the local variable instead. If the rename is unavoidable, pin the logical ID: reach the L1 child through `node.defaultChild` and call `overrideLogicalId` with the value already in the template, or use `Stack.renameLogicalId`. And set a retain removal policy on stateful resources so a mistake costs you an orphan, not a restore.

code

typescript · 20 lines
typescript
import * as cdk from 'aws-cdk-lib';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { Construct } from 'constructs';

export class TableStack extends cdk.Stack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // construct id was renamed from 'Table' to 'MainTable'
    const table = new dynamodb.Table(this, 'MainTable', {
      partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });

    // keep emitting the logical ID already in the deployed template.
    // DO NOT DELETE: removing this replaces the live table.
    const cfnTable = table.node.defaultChild as dynamodb.CfnTable;
    cfnTable.overrideLogicalId('TableCD117FA1');
  }
}

go deeper

for a junior

Know that the construct id you pass as the second argument ends up in the CloudFormation template and is not just a label for humans, so changing it changes what gets deployed.

for a middle

Explain the derivation: the chain of ids forms a path, the path is turned into a logical ID with a hash appended, and CloudFormation identifies resources only by that ID — so a new ID means create-and-delete.

for a senior

Demonstrate the production reflex. Read cdk diff before deploying, pin the old ID with overrideLogicalId when a rename is forced, and have retain policies on stateful resources so the failure mode is an orphan rather than data loss.

for a principal

Own the guardrail. Decide how replacements of stateful resource types are gated in the pipeline, and treat the internal construct tree of any shared library as a versioned interface whose changes ripple into every consumer's next deploy.

## What a logical ID is A CloudFormation template is a map of logical IDs to resource definitions. The logical ID is the identity CloudFormation uses to decide, on every update, whether a resource in the new template is the same resource it already manages. If a logical ID present in the deployed stack is absent from the new template, that resource is deleted. If a logical ID appears that was not there before, it is created. There is no rename operation anywhere in that model. That single fact is the root of this question. Everything else is about how the CDK chooses those IDs on your behalf. ## How the CDK builds one Constructs form a tree. Every construct takes `(scope, id, props)`, and the scope is its parent, so the ids chain into a path: `MyStack/MainTable/Resource`. At synthesis the CDK allocates a logical ID from that path — the readable components joined together, with a short hash of the full path appended so that two constructs with the same leaf id in different branches do not collide. Some structural ids such as `Resource` and `Default` are omitted from the readable part, which is why an `s3.Bucket` with id `Data` becomes something like `Data` plus a hash rather than `DataResource` plus a hash. The important property is that the ID is a **pure function of the path**. Not of the variable name, not of the resource's physical name, not of its position in the file, and not of anything random — which is what makes CDK deploys repeatable. But it also means every part of the path is load-bearing. ## Why the rename destroys the table Renaming the id from `Table` to `MainTable` changes the path, which changes both the readable prefix and the hash. The synthesized template now contains a logical ID CloudFormation has never seen, and no longer contains the one it is managing. The update therefore creates a new empty table and deletes the old one with all its data. The same mechanism fires in cases that look innocent: - Wrapping existing resources in a new construct to tidy the code — every child's path gains a component. - Moving a resource from the stack into a helper construct, or out of one. - Upgrading a shared construct library whose author renamed an internal child id. - Reordering nothing at all, but changing the id casing. None of these are visible as "dangerous" in a diff of the source. They are visible in `cdk diff`, which is exactly why the diff is read before the deploy rather than after it. ## The ways out **Do not rename.** The cheapest option, and the right default. Construct ids are part of the deployed contract, like a database column name. Rename the TypeScript variable, the class, the file — none of those touch the path. Say this first in an interview; the mitigations below are for when the rename is genuinely forced. **Pin the logical ID.** Every L2 construct has an L1 child reachable through `node.defaultChild`. Cast it to the matching `Cfn*` type and call `overrideLogicalId('TheDeployedId')`. Synthesis then emits the old ID under the new construct id, and CloudFormation sees no change at all. The cost is a permanent, slightly odd line of code that you must not delete, so comment it. **Rename at the stack level.** `Stack.renameLogicalId` overrides the generated allocation so the template keeps emitting the ID already deployed. It is the same idea applied without reaching into the child. **Accept the replacement.** For a stateless resource — a Lambda function, a security group, a queue you can drain — replacement may be entirely fine, and pinning IDs forever has its own cost. Decide deliberately rather than by accident. ## Prevention beats recovery The production discipline around this has three parts. Set a retain removal policy, and where the service offers it, deletion protection, on anything stateful — then the worst outcome of a mistaken rename is an orphaned resource and an empty new one, not a restore from backup. Run `cdk diff` in the pull request and make replacements of stateful resource types something a human must acknowledge. And treat any shared construct library's internal tree as an API: if the platform team renames a child id inside a published construct, every consumer's next deploy carries a replacement. ## The comparison worth making Candidates coming from other tooling often look for a "moved" declaration that tells the tool an address changed. The CDK has no such construct: identity is the path, and the remedies are to keep the path or to override the ID it produces. Knowing that difference — and not searching for a feature that does not exist while a production deploy is pending — is the point of the question.

  • Besides renaming an id, what other everyday refactor changes a logical ID?
    Anything that changes a construct's path. Wrapping existing resources in a new parent construct to tidy the code adds a path component to every child; moving a resource into or out of a helper construct does the same. Upgrading a shared construct library whose author renamed an internal child id has the identical effect on your stack, without any change in your own source.
  • How do you make sure a mistaken rename cannot destroy data in the first place?
    Put a retain removal policy, and deletion protection where the service supports it, on every stateful resource. Then a wrong logical ID leaves you with an orphaned resource and an empty new one — recoverable by importing or re-pinning — instead of a restore from backup. Pair it with cdk diff in the pull request so replacements are seen before they run.
  • Why does the CDK append a hash to the readable part of the logical ID?
    To guarantee uniqueness across the tree. Two constructs in different branches can legitimately share a leaf id, and the readable components alone would collide. The hash is computed from the full path, which is also why moving a construct changes the ID even when the visible prefix looks unchanged.

saying these in an interview costs you the question

  • Thinks CloudFormation renames a resource when its logical ID changes
  • Believes the logical ID comes from the variable name in code
  • Assumes the physical resource name keeps the resource alive
  • Looks for a moved or rename declaration the CDK does not have
  • Treats construct ids as cosmetic and safe to tidy up

context