Skip to content

CDKTF: Cross stack dependencies fail if placed within a wrapper construct. #2976

Open
@ahitchin

Description

@ahitchin

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

  1. Create a CDKTF package.
  2. Create a construct that contains your deployment stacks.
  3. Create two or more stacks.
  4. Introduce cross-stack dependencies.
  5. Deploy your scaffolding stack.
  6. Deploy the stacks that depend on the initial scaffolding stack.
  7. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcdktfconfirmedindependently reproduced by an engineer on the teamfeature/multi-stackpriority/important-longtermMedium priority, to be worked on within the following 1-2 business quarters.size/mediumestimated < 1 week

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions