Diff: nested stacks
cdkd diff --recursive looks inside nested stacks. Each nested stack is
compared with its own state, and its changes are printed in a block of their
own. Without --recursive, the diff stops at the parent, as cdk diff does.
# the parent only
cdkd diff ParentStack
# the parent and every nested stack below it
cdkd diff ParentStack --recursive
--fail-on=destructive always looks inside nested stacks, with or without
--recursive.
What --recursive shows
Without the flag, a changed nested stack appears as one line in the parent:
its AWS::CloudFormation::Stack resource, with a TemplateURL or
Parameters change. Nothing inside the nested stack is inspected.
With the flag, each nested stack that has changes gets a block under a
Nested stack: <name> header. The name is the nested stack's full name in
state. It joins the levels with ~, such as ParentStack~Child, and is the
name cdkd state show --show-nested prints. Nested stacks with no changes are
checked and print nothing.
Three situations produce a block where every resource has the same marker:
| The nested stack | Every resource shows as |
|---|---|
| Has no state file yet | A create |
| Was removed from the CDK code | A delete |
Has a Condition that is now false |
A delete |
A removed nested stack is followed all the way down, so the nested stacks inside it show as deletes too. The third row applies only when the diff can evaluate the condition; see Conditions over a secret-fed parameter.
A nested stack whose state record is damaged is reported with a warning; see Diff: incomplete or malformed state.
Parameters passed to a nested stack
The parent passes values to a nested stack through its Parameters. The diff
resolves them the way cdkd deploy does before it compares. A Ref to one of
the parent's parameters, or an Fn::If, is therefore compared as the value
the deploy would send.
A change the preview cannot list
One change is invisible to the diff. A resource in a nested stack can read the
value of a NoEcho custom resource through a stack parameter. The diff sees
*** on both sides, so it reports nothing. The deploy updates that resource
when the parent's custom resource returns a new value.
Cyclic nested templates are refused
The diff refuses a set of templates in which a nested stack contains itself,
directly or through other nested stacks. It exits 1 with this error:
Nested stack Child under stack Parent~Child resolves to nested template
/path/to/cdk.out/child.json, which is already being diffed higher up the
same nesting chain. Its Metadata['aws:asset:path'] closes a cycle; CDK emits
an acyclic nested template tree, so this indicates the synth output was
hand-modified or generated by a non-CDK toolchain. Refusing to diff.
CDK never produces a cycle, so a cdk.out directory that CDK wrote does not
hit this. You can reach it only with synthesis output that was edited by hand
or produced by another tool. The error names the nested stack resource and the
template file that closed the cycle.
The diff finds each nested template through the aws:asset:path entry in the
nested stack resource's Metadata. These cases define what counts as a cycle:
| Case | Result |
|---|---|
| Two sibling nested stacks use the same template | Allowed |
| The same file reached through a symbolic link | Counts as the same template |
A cycle through a nested stack whose Condition is false |
Refused |
| More than 512 levels of nesting | Refused |
| More than 10,000 nested stacks | Refused |
A cycle through a false Condition is refused with the wording cdkd deploy
uses: The nested template tree under stack ... contains a cycle.
Nested templates outside the assembly directory are refused
The diff also refuses a nested template whose path points outside the directory its parent template is in:
Nested stack Child has Metadata['aws:asset:path']=../../etc/passwd which
resolves to /etc/passwd, outside /path/to/cdk.out. CDK emits assembly paths
that stay inside the assembly directory; one that leaves it indicates the synth
output was hand-modified or generated by a non-CDK toolchain. Refusing to load.
Two related paths are refused as well:
- A path that leaves the directory only after a symbolic link is followed.
- An absolute path. A separate check refuses it, and its message says
which is absolute.
cdkd deploy applies the same rules; see
Cyclic nested templates are refused.
Nested stacks and secret references
This section applies when a parent passes a secret to a nested stack as a
parameter. A secret here means a dynamic reference:
{{resolve:secretsmanager:...}}, {{resolve:ssm-secure:...}}, or
{{resolve:ssm:...}} naming a SecureString.
The diff does not decrypt the parameter. It passes the {{resolve:...}} text
down to the nested stack as the parameter's value. The nested stack's state
holds the same text, so the diff compares one reference with another and no
plaintext is printed.
This has one consequence. The diff assumes that the secret behind an unchanged reference has not changed.
A phantom change on a list-typed parameter
A list-typed parameter is split on commas. A reference that contains a comma
in its JSON-key part, such as
{{resolve:secretsmanager:sec:SecretString:a,b::}}, is split in the wrong
place. The diff then reports a change on every cdkd diff --recursive,
although nothing changed. Nothing is written and no plaintext is exposed.
How each parameter Type is compared is in
cdkd diff internals.
Conditions over a secret-fed parameter
A template condition can depend on a parameter that holds a secret, directly or through another condition. The diff cannot evaluate such a condition, because it does not know the secret's value. The same applies to a condition over a template parameter the diff cannot bind. Every other condition is evaluated as usual.
For a condition it cannot evaluate, the diff reuses the answer the last
cdkd deploy recorded in the nested stack's state. It does so only while the
condition, the conditions it names, and the inputs of its parameters are
unchanged since that deploy.
When there is no recorded answer it can reuse, the diff falls back to two rules:
- An
Fn::Ifon the condition takes its false branch. - A resource gated on the condition is kept and compared. It is not removed.
The fallback can be wrong in both directions. It can show a change the deploy will not make. It can also hide one: after a deploy where the condition was false, an edit that makes it true still shows as no change. When a recorded answer exists but no longer matches, the diff warns and names the condition.
There is no recorded answer the diff can reuse in these situations:
- The condition or its inputs changed since the last deploy.
- The state was written by an older cdkd, or by a deploy that failed.
- The condition reads an SSM-typed parameter or a
NoEchoparameter.
The complete list is in cdkd diff internals.
Secret parameters must be Type: String
A nested-stack parameter that receives a secret reference must be declared
Type: String. CDK declares every such parameter as Type: String, so an
app written with CDK does not hit this rule.
cdkd deploy refuses a Type: Number or Type: List<Number> parameter in
that position, and names the parameter:
Nested-stack parameter 'DbPort' is declared 'Type: Number', but the parent
stack resolved a SECRET dynamic reference into it. ...
The reason is how cdkd keeps secrets out of state. It recognises a secret
value as a string and writes the reference back in its place. A value
converted to a number can no longer be recognised, so the decrypted secret
would stay in the nested stack's state. cdkd diff warns for any parameter
whose declared type could cause this.
CommaDelimitedList and the other List<...> types are allowed as long as
the secret value contains no comma. A JSON secret always contains commas, so a
list-typed parameter works only for a secret that is a single plain value.
Related
cdkd diff: the text output, the exit codes and the options- Diff: secrets and NoEcho values: what the diff prints in place of a secret
- Deploy: safety & compatibility flags: the same nested-template refusals on deploy
- cdkd diff internals: how parameters and conditions are compared