cdkd scrub across stacks
cdkd scrub cleans one stack's state file at a time, but
stacks are connected. A nested stack has its own state file under its parent.
A stack can read a secret that another stack exports. And every stack in a
region publishes its exports to one shared index file. This page covers what
scrub does in each case.
# every stack in the app, in dependency order
cdkd scrub --all
# ParentStack and every nested stack under it
cdkd scrub ParentStack
# two stacks; the one that exports is scrubbed first
cdkd scrub DbStack ApiStack
--all
--all scrubs every stack in the synthesized app. It scrubs a stack that
exports a value before the stacks that read that value, and it orders several
named stacks in the same way.
cdkd scrub --all
The order matters because scrubbing a reading stack depends on the exporting
stack: scrub copies the reference from the exporting stack's state. cdkd takes
the order from CDK's stack dependencies and from the Fn::ImportValue and
Fn::GetStackOutput references in the templates.
When scrub refuses one stack, it still scrubs the others. The run then ends
with exit code 2 and names each stack it could not examine, including every
nested stack under a stack that failed. The summary line never counts a stack
the run did not reach.
Edge cases
--all --dry-runon an app that has never been scrubbed. A dry run writes nothing, so the exporting stack is not cleaned before the reading stack is checked. The reading stack is then refused withSCRUB_CROSS_STACK_PRODUCER_PLAINTEXT. That is expected. See The exporting stack still stores the plaintext.- A state bucket shared by several CDK apps.
--allmeans every stack in this app. It does not mean every stack in the bucket. Runcdkd scrub --allfrom each app.
Nested stacks
Scrubbing a stack also scrubs every nested stack (cdk.NestedStack) under it,
at any depth. --all and the --dry-run --fail gate cover nested stacks too.
A nested stack has its own state file, at
<state-prefix>/<Parent>~<Child>/<region>/state.json. It is not one of the
app's stacks, so you cannot name it on the command line. Pass its parent:
cdkd scrub ParentStack
A name in the Parent~Child form matches no stack. Scrub refuses it and
names the parent to pass. When your other arguments did match, scrub runs for
those and warns that the nested stack was not scrubbed.
A nested stack receives a secret through a parameter that its parent passes. So scrub learns the nested stack's secrets from the parent's template, and scrubs each nested stack right after the stack that deploys it.
SCRUB_NESTED_CHILD_UNRESOLVABLE
Scrub refuses a nested stack that has a state file when it cannot work out
which parameter values the parent deployed it with. The nested stack is not
reported clean, the other stacks are still scrubbed, and the run exits 2.
The message says which of these causes applies.
- The parent's template no longer declares the nested stack. Deploy the parent. The deploy destroys the nested stack and removes its state file.
- The parent's state has no entry for the nested stack. Deploy the parent
so that it records the entry, then run scrub again. If a
Conditionkeeps the nested stack out of the deploy, its state file is a leftover. Remove it withcdkd state destroy. - Nothing leads to the state file. Either the parent's own state file was
removed (
cdkd state orphan <Parent>removes only the parent's file), or the entry is missing from both the parent's template and its state. Scrub finds these files by listing the state files under<Parent>~in the parent's region. Inspect the file withcdkd state show. If it is a leftover, remove it withcdkd state destroy(which also deletes its resources) orcdkd state orphan(which removes only the state file). - Scrub of the stack that deploys it failed. That failure is reported too. Fix it and run scrub again.
- A parameter value the parent passes could not be resolved, or the nested
stack's parameter declaration rejects it. The message, or
--verbose, shows which value. - The synthesized template names no template file for the nested stack. Synthesize again with a CDK version that writes it, then run scrub again.
A nested stack that never had a state file is skipped without a message.
The parent's copy of a nested stack's outputs
A parent's state keeps a copy of each nested stack output, as an attribute
named Outputs.<Name> on the nested stack's entry. In a state file written by
an older cdkd, that copy can hold an output's plaintext. After scrubbing each
nested stack, scrub rewrites the parent's copy to match. It reports the change
in a line that starts
Scrubbed N nested-stack output attribute(s) in <Parent>.
Scrub rewrites a copy only when it equals exactly what the nested stack's
output resolves to. A copy that holds a known secret value but equals no output is reported
instead, and --dry-run --fail exits 1 until a deploy of the nested stack
rewrites it.
Edge cases
- A broken tree of nested templates. When the nested templates refer to
each other in a cycle, are nested too deeply, or one points outside the
cloud assembly, scrub refuses the whole top-level stack before it writes
anything. The code is
SCRUB_NESTED_TEMPLATE_TREE_MALFORMED. Synthesize the app again with CDK.
Values read from another stack
A stack can read another stack's exported value with Fn::ImportValue or
Fn::GetStackOutput. When the exported value is a secret, the reading stack's
state can hold it in plaintext too. Scrub replaces that plaintext with the
reference stored in the exporting stack's state.
For that to work, scrub must be able to read the exporting stack's state, and
that state must already hold the reference. cdkd scrub --all arranges both
by scrubbing the exporting stack first.
The exporting stack still stores the plaintext
When the exporting stack has not been scrubbed, its state holds the plaintext
and no reference. Scrub then has nothing to write into the reading stack. It
refuses the reading stack with SCRUB_CROSS_STACK_PRODUCER_PLAINTEXT and
names the exporting stack.
Scrub the exporting stack first, then the reading stack:
cdkd scrub DbStack
cdkd scrub ApiStack
When the value passes through several stacks, scrub them in order, starting
with the stack that declares the secret. cdkd scrub --all does this in one
run.
Scrub refuses only for an export that the templates show to carry a secret:
one the exporting stack declares from a {{resolve:...}} reference, or one it
passes on from a stack further up the chain that does. An ordinary import,
such as a bucket name or an ARN, is unaffected. The exact rules, and four
cases scrub cannot classify, are in
cdkd scrub internals.
The read cannot be resolved
When scrub cannot resolve an Fn::ImportValue or Fn::GetStackOutput, it
cannot learn the value to search for. Typical causes are a deleted exporting
stack and a missing state file. Scrub refuses the reading stack with
SCRUB_CROSS_STACK_READ_UNRESOLVED. Deploy the exporting stack or correct the
reference, then run scrub again.
Scrub does not refuse over a read it does not need. A read inside an Fn::If
branch that is not selected is skipped, and so is an Fn::ImportValue inside
an output that the template's conditions leave out.
Names of values read from another stack
Beside the values, a stack's state records what it read: the export name of
each Fn::ImportValue, and the stack and output name of each
Fn::GetStackOutput.
A name can contain a secret when the template builds it from one:
{
"Fn::ImportValue": {
"Fn::Sub": "{{resolve:secretsmanager:prod/tenant}}-queue-url"
}
}
Here the stored export name contains the tenant secret's value. Scrub replaces the value inside the stored name with the reference. It does this for three fields:
imports[].exportName,outputReads[].sourceStack,outputReads[].outputName.
Scrub leaves sourceRegion and imports[].sourceStack as stored.
cdkd destroy compares imports[].sourceStack with stack names when it
refuses to delete a stack that another stack still imports from. Rewriting the
field would remove that protection.
A name built around a secret that has since been rotated
Scrub cannot repair a stored name whose secret was rotated after the name was written. Scrub knows the secret's current value, and the stored name contains the old one, so nothing matches.
Scrub still reports the name. The stack is not reported clean, and --fail
exits 1. The warning identifies the entry by its position, such as
state.imports[1], and never prints the stored value.
Rotating the secret again does not help. A deploy that updates the stack rewrites both lists of reads. A deploy that finds nothing to change keeps the old entry.
Scrub can report an entry in the same way after you remove its reference from the template, when the entry's name looks like a secret-bearing read of the same exporting stack. The exact test is in cdkd scrub internals.
The exports index
After scrub rewrites a stack's state.json, it also updates that stack's
entries in the exports index, so the index does not keep a plaintext that
state no longer holds.
The exports index is one file per region, at
<state-prefix>/_index/<region>/exports.json. It maps each export name to the
exporting stack's output value, so that an Fn::ImportValue does not have to
read every state file. Every cdkd stack in the region shares it.
For each entry that a scrubbed stack publishes, scrub compares the entry with the stack's stored output of the same name:
- The stored output is a reference or
***, and the entry differs. Scrub sets the entry to the stored output. Until it does, the entry is a finding, and--dry-run --failexits1. - The entry already matches. Scrub writes nothing.
Scrub reads the index under --dry-run as well. So
cdkd scrub --all --dry-run --fail can exit 1 over an index entry even when
every state.json already holds the reference.
Scrub only changes the value of entries the index already has. It adds no export name and removes none.
Edge cases
- An entry could not be written. The run fails with
SCRUB_EXPORT_INDEX_INCOMPLETE(exit2). Fix the cause, which is usually an S3 permission on<state-prefix>/_index/, and run scrub again. The second run writes only the entries that still differ. - An entry has no stored output of the same name. Scrub leaves the entry
as it is. Redeploy the exporting stack, which rewrites its index entries. If
the entry's value still contains a known secret value, it is a finding and
--failexits1. - An entry belongs to a stack this run did not scrub. Scrub reports it for information. It never fails the gate, because another CDK app that shares the bucket may own it.
- A narrowed IAM policy. Writing the index needs no permission beyond
those
cdkd deployuses on the same file. A policy that someone restricted to<state-prefix>/<stackName>/*fails here, as it already fails for deploys that use cross-stack references.
The comparison rules and the wording of each report are in cdkd scrub internals.
Related
cdkd scrub: the commands, options and exit codes- What cdkd scrub covers: which values scrub rewrites and which it cannot see
- cdkd scrub findings and refusals: each message and code, with what to do
- Cross-Stack References: how exports, imports and the exports index work