Description
Expected Behavior
Overview
When deploying a multi-stack application whose stacks are created within a surrounding construct, you should be able to share cross-stack references.
Pseudo-Code
/** ./main.ts */
import { App } from 'cdktf';
import { MultiStackApp } from './multi-stack-app';
/** Entry point for the terraform application. */
function main(): void {
// Create a cdk app to encapsulate the project
const app: App = new App();
// Create a multi-stack service
new MultiStackApp(app, 'MultiStackApp');
return app.synth();
}
/** ./multi-stack-app.ts */
import { App, TerraformStack } from 'cdktf';
import { Construct } from 'constructs';
/** Creates multiple stacks a part of a single service. */
export class MultiStackApp extends Construct {
/** Stacks created by this construct. */
public readonly stackOne: TerraformStack;
public readonly stackTwo: TerraformStack;
/**
* Constructor.
*
* @param scope - Parent app construct.
* @param id - Construct identifier.
*/
public constructor(app: App, id: string) {
// Inherit from the base construct
super(app, id);
// Create the dependency stack
this.stackOne = TerraformStack(this, 'StackOne');
// Create the dependent stack
this.stackTwo = TerraformStack(this, 'StackTwo', {
crossStackValue: this.stackOne.resource.id,
});
return;
}
}
Manifest File (cdktf.out/manifest.json
)
{
"version":"0.17.0",
"stacks":{
"StackOne":{
"name":"StackOne",
"constructPath":"MultiStackApp/StackOne",
"workingDirectory":"stacks/StackOne",
"synthesizedStackPath":"stacks/StackOne/cdk.tf.json",
"annotations":[
],
"dependencies":[
]
},
"StackTwo":{
"name":"StackTwo",
"constructPath":"MultiStackApp/StackTwo",
"workingDirectory":"stacks/StackTwo",
"synthesizedStackPath":"stacks/StackTwo/cdk.tf.json",
"annotations":[
],
"dependencies":[
"StackOne"
]
}
}
}
Actual Behavior
Overview
The CDKTF deployment fails, since it can not find its cross-stack dependency in the cdktf.out/manifest.json
file. This is because a manifest's name points to stack.node.id
, whereas the dependencies point to dependencyStack.node.path
. This results in the dependency stack's manifest name being StackId
, and the dependent stack's dependency reference being ParentConstructId/StackId
.
Error Example
0 Stacks deploying 0 Stacks done 0 Stacks waiting
Error: Usage Error: The following dependencies are not included in the stacks to run: MultiStackApp/StackOne. Either add them or add the --ignore-missing-stack-dependencies flag.
Manifest File (cdktf.out/manifest.json
)
{
"version":"0.17.0",
"stacks":{
"StackOne":{
"name":"StackOne",
"constructPath":"MultiStackApp/StackOne",
"workingDirectory":"stacks/StackOne",
"synthesizedStackPath":"stacks/StackOne/cdk.tf.json",
"annotations":[
],
"dependencies":[
]
},
"StackTwo":{
"name":"StackTwo",
"constructPath":"MultiStackApp/StackTwo",
"workingDirectory":"stacks/StackTwo",
"synthesizedStackPath":"stacks/StackTwo/cdk.tf.json",
"annotations":[
],
"dependencies":[
"MultiStackApp/StackOne"
]
}
}
}
Steps to Reproduce
- Create a CDKTF package.
- Create a construct that contains your deployment stacks.
- Create two or more stacks.
- Introduce cross-stack dependencies.
- Deploy your scaffolding stack.
- Deploy the stacks that depend on the initial scaffolding stack.
- Encounter error.
Versions
language: typescript
cdktf-cli: 0.15.5
node: v18.16.0
cdktf: 0.17.0
constructs: 10.2.61
jsii: null
terraform: 1.4.5
arch: x64
os: linux 5.10.16.3-microsoft-standard-WSL2
Providers
┌───────────────┬──────────────────┬─────────┬────────────┬─────────────────────────┬─────────────────┐
│ Provider Name │ Provider Version │ CDKTF │ Constraint │ Package Name │ Package Version │
├───────────────┼──────────────────┼─────────┼────────────┼─────────────────────────┼─────────────────┤
│ archive │ 2.4.0 │ ^0.17.0 │ │ @cdktf/provider-archive │ 7.0.0 │
├───────────────┼──────────────────┼─────────┼────────────┼─────────────────────────┼─────────────────┤
│ aws │ 4.67.0 │ ^0.17.0 │ │ @cdktf/provider-aws │ 15.0.0 │
├───────────────┼──────────────────┼─────────┼────────────┼─────────────────────────┼─────────────────┤
│ local │ 2.4.0 │ ^0.17.0 │ │ @cdktf/provider-local │ 7.0.0 │
├───────────────┼──────────────────┼─────────┼────────────┼─────────────────────────┼─────────────────┤
│ null │ 3.2.1 │ ^0.17.0 │ │ @cdktf/provider-null │ 7.0.0 │
├───────────────┼──────────────────┼─────────┼────────────┼─────────────────────────┼─────────────────┤
│ random │ 3.5.1 │ ^0.17.0 │ │ @cdktf/provider-random │ 8.0.0 │
└───────────────┴──────────────────┴─────────┴────────────┴─────────────────────────┴─────────────────┘
Gist
No response
Possible Solutions
Overview
Update the Manifest
class so StackManifest
names are equal to stack.node.path
, or so that dependencies are equal to stack.node.id
Relevant Code:
https://github.com/hashicorp/terraform-cdk/blob/main/packages/cdktf/lib/manifest.ts#L41-L64
Possible Code Solution
export class Manifest implements IManifest {
public forStack(stack: TerraformStack): StackManifest {
const node = stack.node;
const name = node.path; // <-------
if (this.stacks[name]) {
return this.stacks[name];
}
const manifest: StackManifest = {
name,
constructPath: node.path,
workingDirectory: path.join(Manifest.stacksFolder, node.id),
synthesizedStackPath: path.join(
Manifest.stacksFolder,
node.id,
Manifest.stackFileName
),
annotations: [], // will be replaced later when processed in App
dependencies: stack.dependencies.map((item) => item.node.path),
};
this.stacks[name] = manifest;
return manifest;
}
Workarounds
Overview
Currently, I am using a custom synthesis function to create the manifest, then using a type modifier to create a mutable copy. After this, I override the dependencies
attribute.
Example Code
import { ISynthesisSession, StackManifest, TerraformStack } from 'cdktf';
import { addCustomSynthesis } from 'cdktf/lib/synthesize/synthesizer';
import { Construct } from 'constructs';
/** Custom synthesis manifest override example stack. */
export class ExampleStack extends TerraformStack {
/**
* Constructor.
*
* @param scope - Parent construct.
* @param id - Construct identifier.
* @param props - Properties needed to create this construct.
*/
public constructor(scope: Construct, id: string) {
// Inherit from the base terraform stack
super(scope, id);
// Inject a custom synthesis function
addCustomSynthesis(this, {
onSynthesize: this.customSynthesis.bind(this),
});
return;
}
/**
* Overrides the stack manifest's dependencies attribute.
*
* @param session - Single session of synthesis.
*/
private customSynthesis(session: ISynthesisSession): void {
// Create a type modifier and make the stack manifest mutable
type MutableStackManifest = {
-readonly [K in keyof StackManifest]: StackManifest[K]
}
// Get the manifest and make a mutable shallow copy
const manifestImmutable: StackManifest = session.manifest.forStack(this);
const manifestMutable: MutableStackManifest = manifestImmutable;
// Override the manifest's dependency references
manifestMutable.dependencies = this.dependencies.map((item) => item.node.id)
return;
}
}
Anything Else?
No response
References
No response
Help Wanted
- I'm interested in contributing a fix myself
Community Note
- Please vote on this issue by adding a 👍 reaction to the original issue to help the community and maintainers prioritize this request
- Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request
- If you are interested in working on this issue or have submitted a pull request, please leave a comment