Skip to content
cdkd

Destroy, state and locking

cdkd destroy works from the state file alone, so this page covers the two together: what a destroy does, where state is stored and what it records, and how the lock keeps two commands from changing the same stack at once. Architecture has the overview.

What cdkd destroy does

cdkd destroy MyStack

A destroy reads the stack's state and deletes every resource recorded there, dependents first. It does not need the template, because the state file already records which resources exist and which resources each one depends on.

Destroying a stackcdkd destroy goes through the CLI layer (a stack name, --app, --force or --all). The state layer gets the state, rebuilds the DAG from its recorded dependencies and applies the implicit type-based delete dependencies. The deployment layer sorts it in reverse topological order, and the provisioning layer calls each provider’s delete() in reverse dependency order.$ cdkd destroyCLI layerdestroy.ts<stackName>, --app, --force, --all (synth-based)State layerGet stateRebuild the DAG from state.dependenciesApply the implicit type-baseddelete dependencies(analyzer/implicit-delete-deps.ts)Deployment layerReverse topological sort: delete in reverseProvisioning layerProvider.delete() in reverse dependency orderDestroying a stackcdkd destroy goes through the CLI layer (a stack name, --app, --force or --all). The state layer gets the state, rebuilds the DAG from its recorded dependencies and applies the implicit type-based delete dependencies. The deployment layer sorts it in reverse topological order, and the provisioning layer calls each provider’s delete() in reverse dependency order.$ cdkd destroyCLI layerdestroy.ts<stackName>, --app, --force, --all (synth-based)State layerGet stateRebuild the DAG from state.dependenciesApply the implicit type-based deletedependencies (analyzer/implicit-delete-deps.ts)Deployment layerReverse topological sort: delete in reverseProvisioning layerProvider.delete() in reverse dependency order

The order comes from two sources:

  • The recorded dependencies. Each resource in state lists the logical ids it depends on. cdkd rebuilds the dependency graph from those lists and walks it in reverse, so a resource is deleted before the resources it depends on.
  • Rules by resource type. AWS enforces some delete orders that no reference in the template expresses. For example, a Lambda function in a VPC leaves a network interface in its subnet for some time after the function is deleted, so every subnet is deleted after every Lambda function. These rules are in src/analyzer/implicit-delete-deps.ts.

Each delete goes through the provider for that resource. The delete phase of a deploy applies the same type rules.

Where state is stored

State lives in one S3 bucket. cdkd uses no DynamoDB table. Each stack has its own prefix for each region:

s3://cdkd-state-123456789012/cdkd/
├── MyStack/
│   └── us-east-1/
│       ├── state.json    # resources, outputs, schema version
│       └── lock.json     # present while a command holds the stack
└── _index/
    └── us-east-1/
        └── exports.json  # export name -> producing stack, for Fn::ImportValue

The cdkd/ segment is the default value of --state-prefix. Because the region is part of the path, the same stack name in two regions has two independent state files.

What the state file records

state.json holds one record for each resource in the stack. A record has five parts:

  • the physical id, which is the id AWS knows the resource by,
  • the resource type,
  • the resolved properties,
  • the attributes that Fn::GetAtt reads,
  • the logical ids the resource depends on.

The full schema and its version history are in State Management.

How the stack lock works

A command that changes a stack first takes the stack's lock, so that two commands cannot change the same stack at the same time. The lock is the S3 object lock.json, and cdkd writes it with conditional requests. S3 itself refuses the request when the condition does not hold, which is why no separate lock service is needed. The code is in src/state/lock-manager.ts.

Step Request Succeeds only when
Acquire PutObject with If-None-Match: * No lock exists
Renew PutObject with If-Match: <etag> This process still holds the lock
Release DeleteObject with If-Match: <etag> The lock is this process's own

A lock carries an expiry time 30 minutes ahead, and a running command keeps renewing its lock in the background. If a command dies without releasing its lock, the lock expires, and the next command to acquire the lock clears it.

To clear a stuck lock by hand, run cdkd force-unlock.

Last updated: