cdkd export (hand a stack over to CloudFormation)
cdkd export <stack> is the mirror of cdkd import (AWS → cdkd) in
the reverse direction (cdkd → CloudFormation). It builds a CFn
ChangeSetType=IMPORT changeset from cdkd state + the synthesized
template, executes it, and deletes cdkd state on success. AWS resources
are unchanged across the migration.
cdkd export MyStack # confirmation prompt; CFn stack name = cdkd stack name
cdkd export MyStack --cfn-stack-name MyStack-CFn
cdkd export MyStack --dry-run # print the import plan, no CFn calls
cdkd export MyStack --template path.json # pre-rendered template (JSON or YAML — format auto-detected, skip synth)
cdkd export # auto-detect single-stack apps
Flow:
Synthesize the CDK app (or read
--template <path>) to get the CloudFormation template.Load cdkd state for the target stack; build the
(logicalId, physicalId, resourceType)map.Refuse if a CFn stack with the destination name already exists, or if any template resource is in the blocked set (template resources without a cdkd state entry; resources whose recorded properties hold the redaction mask
***, i.e. aNoEchocustom-resource value cdkd cannot re-derive. Forcing that custom resource to update does NOT clear this block: the handler supplies the value to the DEPLOY and cdkd re-masks it on the way into state, which is what the export reads. Stop settingNoEchoon that response and re-deploy, then export again; or export the stack without that resource and adopt it by hand. OrAWS::CloudFormation::Stackrows whose parent cdkd state has no matching nested-stack entry). Lambda-backed Custom Resources (Custom::*ANDAWS::CloudFormation::CustomResource— the latter is whatnew cdk.CustomResource(...)synthesizes when noresourceTypeis passed) are NOT blocked but require--include-non-importableto run the 2-phase flow described below.AWS::CloudFormation::Stackrows whose parent state has a matching nested-stack entry are classified into a dedicatednestedStackRowslist and exported via the per-stack IMPORT loop: the orchestrator recursively walks the cdkd state tree viabuildCdkdStateStackTreeand submits IMPORT changesets per cdkd-managed stack in leaf-first order. Leaf stacks get a single CREATE-via-IMPORT changeset; non-leaf parents get two per parent (Phase 1A CREATE-via-IMPORT for the parent's leaf resources only, then Phase 1B UPDATE-via-IMPORT against the now-existing parent to adopt the already-IMPORTed children via the AWS-docs "Nest an existing stack" pattern). Phase 1B injectsDeletionPolicy: RetainplusResourceIdentifier: { StackId: <child arn> }plus aTemplateURLrewritten to point at the child's AWS-canonicalized template fetched viaGetTemplate(Processed)post-IMPORT plus child Tags forwarded fromDescribeStacks(AWS's "Nested stack import validation" rejects tag mismatches). Between phases each non-root stack is flipped fromIMPORT_COMPLETEtoUPDATE_COMPLETEvia a no-op tag-onlyUpdateStack(AWS rejectsIMPORT_COMPLETEas a non-importable status for nesting; the flip adds a transientcdkd:nested-export-fliptag that Phase 1B then forwards verbatim into the parent template). Each child cdkd stack<parent>~<childLogicalId>becomes its own CFn stack named<parent>-<childLogicalId>by default (~is illegal in CFn stack names); per-child overrides via--cfn-child-stack-name '<cdkdName>=<cfnName>'(repeatable). Per-child Parameters are forwarded from the parent template'sAWS::CloudFormation::Stack.Properties.Parametersblock — literal string / number / boolean values pass through, and intrinsic-valued Parameters ({Ref: <ParentParam>}/{Fn::GetAtt: [ParentResource, Attr]}) are resolved at IMPORT time against the parent's resolved Parameters + cdkd state (a root-first pre-pass, since a child's Parameters resolve against its parent's). A value cdkd cannot resolve degrades to a warning and the child template's ParameterDefaultmust cover it. The original "one atomic--include-nested-stacksIMPORT changeset" design was found infeasible by the 2026-05-24 AWS spike — AWS rejects that flag combination withValidationError: IncludeNestedStacks is not supported for changeSet type: IMPORT; see docs/design/464-nested-stacks-export-import.md §4.0 / §4.3 for the per-stack-loop algorithm.--dry-runprints the per-stack plan summary without acquiring child locks or submitting any changeset.Resolve each resource type's primary identifier property name(s) via
cloudformation:DescribeType(with a hardcoded fallback table for ~30 single-key types). Composite primary identifiers (primaryIdentifier.length > 1) are supported forAWS::ApiGateway::Method,AWS::ApiGateway::Resource,AWS::ApiGateway::Deployment,AWS::ApiGateway::Stage,AWS::ApiGateway::Authorizer,AWS::ApiGateway::Model,AWS::ApiGateway::RequestValidator,AWS::EC2::VPCGatewayAttachment,AWS::ApiGatewayV2::Integration,AWS::ApiGatewayV2::Route,AWS::S3Tables::Namespace, andAWS::Lambda::Permissionvia a per-type splitter that maps cdkd'sphysicalId(plus the resource's recordedpropertiesfor sub-resource types where the parent identifier —ApiId/FunctionName— lives inproperties, not inphysicalId) to the field mapResourceIdentifierexpects. Sub-resource types whose primaryIdentifier includes an AWS-generated id (IntegrationId/RouteId/ Lambda::Permission'sId) narrow thePropertiesoverlay to the writable subset so CFn doesn't reject the changeset with "Encountered unsupported property". Other composite types abort with a clear error pointing at where to register a new splitter insrc/cli/commands/export.ts. IMPORT-unsupported types (CFn schema lacks the handlers needed for IMPORT lookup — eitherhandlers: []outright, or noread/listhandler so CFn can't look the resource up by identifier) are auto-handled via a pre-delete + phase-2-CREATE dance: cdkd skips the resource from phase 1, deletes the AWS-side resource between phases via the appropriate SDK call, and lets CFn re-CREATE in phase 2. Currently registered:AWS::ApiGatewayV2::Stage(handlers: []; auto-emitted by CDK'sHttpApiconstruct as$default; pre-delete viaapigatewayv2:DeleteStage). Brief unavailability window ~10s; HttpApi endpoint URL is unchanged because it embeds ApiId, not StageName.AWS::IAM::Policy(handlers: ['create', 'delete', 'update']— noread/listbecause inline policy attachments have no first-class AWS resource id; auto-emitted by CDK L2 grants such as ECS Task Execution Role ECR pull policy and Lambda execution role inline policies; pre-delete viaiam:DeleteRolePolicy/DeleteUserPolicy/DeleteGroupPolicyper attachment target). The inline policy attachment is dropped from each Role / User / Group between phases — any in-flight AWS API call that depends on the granted permission will fail withAccessDenieduntil CFn re-CREATEs in phase 2.
Pass
--no-recreate-import-unsupportedto block instead of auto-handling. Per-type config lives inIMPORT_UNSUPPORTED_RECREATABLE_TYPESandPRE_DELETE_HANDLERSinsrc/cli/commands/export.ts.Types whose cdkd physical id is composite while the CFn identifier is a single field are resolved from the value cdkd RECORDED, not from the id. CFn identifies an
AWS::S3Tables::Tableby itsTableARNand anAWS::AppSync::DataSource/::Resolverby itsDataSourceArn/ResolverArn, while cdkd's physical id for each is the pipe-joined tuple its provider needs (<tableBucketARN>|<namespace>|<name>,<apiId>|<name>,<apiId>|<typeName>|<fieldName>) — and the correct identifier is not a segment of that tuple, so the composite-id splitters above cannot produce it. cdkd reads the ARN from the resource's recordedattributesinstead, and blocks the resource with an actionable message when state does not carry it (a record written before cdkd started recording the ARN — re-deploy the stack once to heal it, as state-management.md describes).AWS::EC2::SecurityGroupIngressis the fourth member and works the same way: CFn identifies a rule by thesgr-...id AWS mints, which cdkd records as the rule'sIdattribute while its physical id stays the<groupId>|<ipProtocol>|<fromPort>|<toPort>tuple the revoke path needs. Its remedies differ from the other three, because there are two ways to lack the attribute. A rule declaring more than one source (bothCidrIpandCidrIpv6on one resource) makes AWS mint one rule per source, and cdkd records neither id — neither is "the" identifier — so re-deploying never heals it; split it into one ingress resource per source. A rule whose record simply predates that recording carries noIdat all, and a no-op re-deploy does not heal that one either, since AWS returns the rule id only fromAuthorizeSecurityGroupIngressitself.cdkd exportrecovers it for you: a row with no usable recordedIdtriggers a paginatedDescribeSecurityGroupRuleson the group the physical id names, and the rule is adopted only when EXACTLY ONE ingress rule on that group carries the composite's(protocol, port range)tuple. Zero matches is REFUSED naming the row and the tuple cdkd searched for — nothing matched, so there are no candidates to name; more than one is REFUSED naming the row and every candidatesgr-…id, since two rules sharing that tuple are two rules cdkd's own physical id cannot tell apart either. Matching rules are counted BEFORE any is set aside, so a rule AWS reports without a usablesgr-…id refuses too rather than letting its sibling pass as "exactly one" — and when more than one rule matched, that refusal carries the two-cause remedy below as well. "More than one" has two causes: the multi-source rule above (split the resource), and two DISTINCT ingress resources differing only by SOURCE, which the composite carries none of — those are already one resource per source, so repair the row'sattributes.Idor remove the row before exporting. The lookup needsec2:DescribeSecurityGroupRules; without that permission the row is blocked with a message saying so, while a THROTTLED lookup is retried with backoff and reported as a throttle, not as a missing permission. A row whose state already records theId— everything a current cdkd deploys — issues no live read at all.Types CloudFormation cannot IMPORT at all are refused up front. A type whose registry schema declares no
readhandler AND reportsProvisioningType: NON_PROVISIONABLEis rejected byCreateChangeSetwithResourceTypes [<T>] are not supported for Import—AWS::Glue::Table,AWS::Route53::RecordSet,AWS::Route53::RecordSetGroup,AWS::AppSync::ApiKey,AWS::EC2::NetworkAclEntry,AWS::SQS::QueuePolicyandAWS::SNS::TopicPolicyare all in this class (measured live, us-east-1, 2026-08-13). cdkd surfaces them from the schema it already fetches for the identifier and names EVERY offending resource in one message — AWS's own error is not exhaustive (a probe carrying three unsupported types named two of them). Both signals must agree before cdkd refuses, so a partial or unusualDescribeTyperesponse falls back to letting AWS answer.The whole plan — this refusal and every other
blockedreason — is built BEFORE the stack lock is acquired, so a stack that cannot be exported says so without first locking out concurrentcdkd deploy/cdkd destroy. Planning issues no AWS write (it reads cdkd state andDescribeType), and the nested-stack path has always planned before locking.Remove the resource from the stack before exporting (it stays in AWS and can be re-declared in CloudFormation afterwards), or destroy it first and let CloudFormation create it fresh. The verdict is a registry HEURISTIC rather than AWS's published supported-for-import list, so
--skip-import-support-preflightis the escape hatch if AWS has since made the type importable — the changeset is then submitted and CloudFormation answers for itself.Acquire the stack lock so concurrent
cdkd deploycannot race.Confirm with the user (skipped with
-y/--yes).Preprocess the phase-1 template (automatic; required by CFn IMPORT contract):
Strip Outputs entirely. CFn rejects IMPORT changesets that declare ANY Outputs with "you cannot modify or add [Outputs]". Phase 2 UPDATE re-submits the full synth template and restores Outputs along with the non-importable resources.
Inject
DeletionPolicy: Deleteon resources that lack the attribute. CFn IMPORT requiresDeletionPolicyon every imported resource, and CDK synth only emits it whenRemovalPolicyis explicitly set. cdkd injectsDelete(notRetain) so the post-export CFn template matches the CFn type-default — same as what plain CFn would have applied for a resource without explicitRemovalPolicy. The user sees no surprisingRetainattribute and the post-exportcdk diffhas no DeletionPolicy noise.UpdateReplacePolicyis intentionally NOT injected (onlyDeletionPolicyis required for IMPORT).Conditional overlay of
ResourceIdentifierontoProperties. Mirrors upstreamcdk importbehavior: pass the synth template through and let CFn match resources viaResourcesToImport[].ResourceIdentifier(the changeset API parameter) alone, except when the synth template carries a literal value for the field that differs fromResourceIdentifier. Four cases:- Absent (auto-generated names — user did NOT declare a
physical name in CDK code):
Properties[<NameField>]stays absent. CFn accepts the IMPORT changeset usingResourceIdentifieralone (verified against AWS in upstreamcdk import). Post-exportcdk diffis clean because both CFn-managed template and CDK synth have the property absent. - Intrinsic (composite-id sub-resources whose synth references
the parent via
{Ref: ...}/{Fn::GetAtt: ...}— Integration / Route / Lambda::Permission / API Gateway Method etc.): the intrinsic is preserved. CFn resolves it during changeset processing against the parent's ownResourceIdentifier(the parent is imported in the same changeset), so the resolved value equalsResourceIdentifier[<field>]and CFn accepts. Post-exportcdk diffstays clean (both sides keep the intrinsic shape). - Literal-mismatch (pre-v0.94.0 prefix-on-user-declared-name
legacy: user wrote
roleName: 'foo'in CDK code; cdkd's deploy prefixed it to'CdkSampleStack-foo'on AWS): overrideProperties.RoleNamefrom the unprefixed CDK value to the prefixed AWS value. CFn's identifier-match check requires this — otherwise AWS rejects withThe Identifier [<Field>] for resource [...] does not match the identifier value for the resource in the template. The overlay persists into the post-import CFn template; the nextcdk deployproposes REPLACE — same caveat as upstreamcdk importwith mismatched-name CDK code (see the "Replacement risk on next deploy" caveat below). The prefix-migration pre-flight is meant to surface this before export. v0.94.0+ stacks with the default--no-prefix-user-supplied-namesflip are NOT in this case —Properties.RoleNamematches the AWS name without override. - Unrepresentable (a list carrying an intrinsic, a nested
list, or an empty list): the export REFUSES, naming the
resource, the property and the scalar to declare instead.
"Literal" was widened
past
typeof === 'string'for the three cases above, because a field the template carries as an ARRAY is neither absent nor an intrinsic and used to survive into the phase-1 template, where CFn answered with an opaque rejection — reachable viaaddPropertyOverride('Namespace', ['analytics'])onAWS::S3Tables::Namespace, which cdkd deploys because the provider accepts both wire shapes. A plain literal (string, number, boolean, or an array of those) is now OVERWRITTEN with the scalar identifier, which is not a guess: the overlay value comes from the cdkd-recorded physicalId, the authority on what the resource IS. Only the shapes above are refused, because preserving any of them reproduces the opaque rejection, while what makes overwriting wrong differs per shape — only a list that actually carries an object element has an intrinsic to discard, so the message says which reason applies.
Pre-v0.95 cdkd unconditionally injected
ResourceIdentifiervalues intoPropertieseven when the synth had no value for that field, baking cdkd-prefixed auto-gen names AND composite-id literals into the post-export CFn template → post-exportcdk diffproposed REPLACE on every auto-named resource and every composite-id sub-resource (defeating the migration's "AWS resources unchanged" promise). v0.95+ overlay is conditional; only the literal-mismatch legacy case still carries the documented post-export caveat.- Absent (auto-generated names — user did NOT declare a
physical name in CDK code):
CreateChangeSet --change-set-type IMPORT→ wait →ExecuteChangeSet→waitUntilStackImportComplete. On failure cdkd fetchesDescribeStackEventsand surfaces the per-resource failure reasons (the waiter alone only reports the high-level rollback state).Delete cdkd state for the migrated stack.
Release lock.
MVP scope (intentional cuts; lift in follow-up PRs):
- JSON and YAML templates supported. Both formats round-trip through
cdkd's CFn-aware codec (
src/cli/yaml-cfn.ts), which preserves every CFn shorthand intrinsic (!Ref,!Sub,!GetAtt,!Join, …) across the parse → preprocess → re-serialize cycle. The phase-1 IMPORT and phase-2 UPDATE changesets emit in the same format as the source template — a YAML-authored CFn stack stays YAML on the wire. - Cross-stack consumer scan runs at synth time when other stacks in
the same CDK app reference the exporting stack via
Fn::GetStackOutput. Those consumers keep resolving after the migration via the CloudFormation fallback (weak reference), so by default cdkd warns that the references become fallback-dependent (they break only for consumers deployed with--no-cfn-fallback);--strict-cross-stackrefuses instead. WithoutFn::GetStackOutput(or with consumer stacks outside the CDK app), no scan can run and the user is responsible for the check. - Drift baseline pre-flight surfaces a warning when cdkd state lacks
observedPropertiesfor one or more resources. Without that baselinecdkd driftcannot reliably compare against AWS, so the nextcdk deploypost-migration may surface unexpected changes if AWS has drifted from the synth template. Resolve by runningcdkd state refresh-observed <stack>(or any redeploy) before exporting, thencdkd drift <stack>to verify. Non-blocking by design — the user decides whether to proceed. - Template Parameters in the synthesized template are forwarded to
both phase-1 and phase-2 changesets. Each parameter is resolved in
order: (1)
--parameter Key=ValueCLI override (repeatable), then (2) the template'sDefault. A parameter with neither override nor default aborts with a clear error listing which keys are missing. A--parameteroverride for a key the template does not declare is also rejected (catches typos). CDK-generated templates typically only carryBootstrapVersionwith a default;cdkd exportworks without any--parameterfor those. - Lambda-backed Custom Resources (
Custom::*ANDAWS::CloudFormation::CustomResource) require--include-non-importableto opt into the 2-phase flow: phase 1 IMPORT changeset for the importable resources, then phase 2 UPDATE changeset for the full template — CFn CREATEs the Custom Resources, which re-invokes each backing Lambda's onCreate handler. The handler must be (1) idempotent (samePhysicalResourceId/Dataon every event type) AND (2) correctly do the cfn-response protocol (PUT a Status/PhysicalResourceId payload toevent.ResponseURL). cdkd's deploy path also accepts a return-value fast path for handler responses, but CFn-side phase-2 UPDATE / future rollback / futurecdk deployagainst the imported stack all require the actual ResponseURL POST — a CR backed by a return-only Lambda will time out at the CFn 1-hour Custom Resource ceiling. Without the flag, the CR types in the template cause the command to abort.AWS::CloudFormation::Stack(nested stacks) is fully supported: the dedicated branch +buildCdkdStateStackTreewalker recursively loads every child state file, validates the tree shape, andrunPerStackImportLoopsubmits one IMPORT changeset per cdkd-managed stack in the tree in leaf-first order. Non-leaf parents adopt their just-imported children via the AWS-docs "Nest an existing stack" pattern (the original--include-nested-stacksdesign was found infeasible by the 2026-05-24 AWS spike — see design/464-nested-stacks-export-import.md §4.0 / §4.3 for the per-stack-loop algorithm). On per-stack failure, cdkd state for the failed stack and every yet-to-be-imported stack is preserved; the error message names which stacks moved and which remain so the user can re-runcdkd export <parent>after fixing the underlying cause (already-imported children will be re-adopted as nested references on retry). On phase-2 failure, cdkd state is preserved and the error message includes the recovery procedure (aws cloudformation create-change-set --change-set-type UPDATE ...followed bycdkd state orphan). - Inline
TemplateBodyonly (51,200-byte cap). Templates larger than that require S3 upload viaTemplateURL; not yet implemented. - Synth template used verbatim: cdkd does NOT substitute
observedPropertiesinto the template. If the CDK code has drifted from the AWS-current state, the nextcdk deployafter migration will update the resource. Runcdkd driftbefore exporting if drift matters.
Context preservation (CLI -c is refused by default):
CDK reads context from cdk.json and cdk.context.json on every
synth. CLI -c key=value overrides are NOT persisted to either file
— they apply only to the current invocation. If you run cdkd export -c env=prod and later run cdk deploy without the same -c env=prod,
CDK synthesizes a different template, which CFn sees as drift / a
replacement on the first post-migration deploy.
cdkd export refuses by default when CLI -c overrides are present.
Two ways forward:
- Recommended: move the overrides into
cdk.json's"context": { ... }field, then re-runcdkd exportwithout-c. Subsequentcdk deployinvocations readcdk.jsonautomatically. - Escape: pass
--accept-transient-context. cdkd proceeds and emits a warn that names every override. You are then responsible for passing the SAME-cflags to every futurecdk deployfor this stack (or moving them tocdk.jsonbefore then). On success, cdkd prints the exactcdk diff/cdk deploycommand including the captured flags.
Caveats:
Replacement risk on next deploy (post-v0.95, only one residual case):
- Pre-v0.94.0 prefix legacy (
--prefix-user-supplied-namesopt-in, or stacks deployed before v0.94.0 flipped the default): cdkd's deploy prefixed user-declared physical names with the stack name for cross-stack uniqueness (e.g.roleName: 'my-role'becameMyStack-my-roleon AWS). The phase-1 IMPORT preprocessing rewrites the template's name field to the prefixed value (otherwise CFn IMPORT rejects the identifier mismatch), and this prefixed value persists into the post-import CFn template. The nextcdk deploywill seeMyStack-my-role(CFn-recorded) vsmy-role(CDK-declared) as a property change on an immutable name field → REPLACEMENT. Before the first post-export deploy, either change the CDK code to the prefixed value (roleName: 'MyStack-my-role') or accept the replacement. The prefix-migration pre-flight (prefix-migration-check.ts) is meant to surface this before export.
No longer in this category as of v0.95:
- Auto-generated names (user did NOT declare
bucketName: '...'etc.): cdkd's overlay used to bake the cdkd-prefixed name into the post-export CFn template, causing every auto-named resource to be proposed for REPLACE on nextcdk deploy. Post-v0.95 the overlay is conditional and skipped for this case → post-exportcdk diffis clean for auto-gen names. - Composite-id sub-resources (
AWS::ApiGateway::Method/AWS::ApiGatewayV2::Integration/AWS::ApiGatewayV2::Route/AWS::Lambda::Permissionetc.): cdkd's overlay used to overwriteProperties.ApiId(intrinsic{Ref: ...}) with the resolved literal parent id, causing every composite sub-resource to be proposed for REPLACE on nextcdk deploy. Post-v0.95 intrinsics are preserved → post-exportcdk diffis clean for composite sub-resources.
When the legacy prefix case applies, check the post-import changeset (
aws cloudformation create-change-set --change-set-type UPDATE) for surprises before executing your first post-exportcdk deploy.- Pre-v0.94.0 prefix legacy (
Cross-stack
Fn::GetStackOutputconsumers in other cdkd stacks keep working after the export via the CloudFormation fallback: the exported stack's outputs move to CloudFormation, and the consumers' next resolve reads them from there (DescribeStacks) after the cdkd-state miss. The reference stays weak either way. Only when deploying consumers with--no-cfn-fallbackdoes the pre-fallback constraint return — plan multi-stack migrations from the leaves up in that case.
Exits 0 on success, 1 on any failure (changeset rejection, AWS
auth, lock contention, etc.). cdkd state is deleted only after the
import changeset completes successfully; a mid-flow failure leaves
cdkd state intact and the user can re-run the command.