'Reuse a parent CDK Stack in other App project

First at all, it's important mention that I'm not using CDK as usual. Instead I'm creating resources on-the-fly programatically. So, basically, I have a multi-tenant application that on onboard it's created a customer root stack, that will be included nested stacks with resources during the customer lifetime.

At the first time, a code like this was executed:

import {App, Stack, Construct, NestedStack} from '@aws-cdk/core';

const main = async() => {
    const app = new App();

    const rootStack = new (class RootStack extends Stack {
        constructor() {
            super(app, `Customer-123-RootStack`, {});
        }
       
    });

    const tenantIamStack = new (class CustomerIamNestedClass  extends NestedStack {
        constructor() {
            super(rootStack, `BasicIAM`, {});

            const cognitoFederatedPrincipal = new iam.FederatedPrincipal('cognito-identity.amazonaws.com', {
                'StringEquals': {
                    'cognito-identity.amazonaws.com:aud': process.env.SHARED_IDENTITY_POOL_ID
                },
                'ForAnyValue:StringLike': {
                    "cognito-identity.amazonaws.com:amr": "authenticated"
                }
            }, 'sts:AssumeRoleWithWebIdentity');

            new iam.Role(this, 'IamRoleTenantUser', {
                roleName: `Customer-123-TenantUser`,
                assumedBy: cognitoFederatedPrincipal,
            });
        }
    });
}

main().then(() => console.log('done'));

I want now to reuse the root Stack Customer-123-RootStack on other Stacks created in other routines. For example, a customer will create an other AWS resource in our platform, like a AWS EventBridge rule or a ACM certificate.

If execute the same code with another nested stack, the first nested stack will be deleted.

import {App, Stack, Construct, NestedStack} from '@aws-cdk/core';

const main = async() => {
    const app = new App();

    const rootStack = new (class RootStack extends Stack {
        constructor() {
            super(app, `Customer-123-RootStack`, {});
        }
       
    });

    const tenantAcmStack = new (class CustomerAcmNestedClass  extends NestedStack {
        constructor() {
            super(rootStack, `BasicACM`, {});

            //create ACM certificate
        }
    });
}

main().then(() => console.log('done'));

I have read this documentation but can't figure out how do that: https://docs.aws.amazon.com/cdk/v2/guide/resources.html#resources_importing

I'm able to use SDK to get the stack but not understand how make it work with CDK.

Edit: to make my question more clear: I need to referente a parent existing stack (created by other app, in other moment, in other code base) to a new NestedStack.



Solution 1:[1]

I found a solution and created this example repository:

I have made a mix with AWS SDK CloudFormation module:

src/index.js:

import * as fs from "fs";
import * as AWS from 'aws-sdk';
import * as cfninc from '@aws-cdk/cloudformation-include';
import * as tmp from 'tmp';
import CdkDeployAbstraction from "./cdk/cdkDeployAbstraction";
import * as dotenv from "dotenv";
import {ReusableRootStackWithNestedStacks} from './cdk/ReusableRootStackWithNestedStacks';
import {OtherStack} from "./cdk/OtherStack";
import {App} from "@aws-cdk/core";

dotenv.config()

AWS.config.region = process.env.AWS_REGION;
AWS.config.credentials = {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
};

const getNestedsStacks = async (stackName: string) => {
    const cloudformation = new AWS.CloudFormation();
    const resourcesResult = await cloudformation.describeStackResources({
        StackName: stackName
    }).promise();

    const resources = resourcesResult.StackResources;
    const filteredResources = resources.filter(value => value.ResourceType === 'AWS::CloudFormation::Stack');

    let nestedStacks: { logicalResourceId: string, physicalResourceId: string; stack: AWS.CloudFormation.Stack }[] = [];
    for (let i = 0; i < filteredResources.length; i++) {
        const resource = filteredResources[i];

        const nestedStackResult = await cloudformation.describeStacks({
            StackName: resource.PhysicalResourceId
        }).promise();

        nestedStacks.push({
            logicalResourceId: resource.LogicalResourceId,
            physicalResourceId: resource.PhysicalResourceId,
            stack: nestedStackResult.Stacks.shift()
        });
    }

    return nestedStacks;
}

const getNestedsStackDetails = async (stackName: string) => {
    const cloudformation = new AWS.CloudFormation();
    const currentNestedStacks = await getNestedsStacks(stackName);

    let nestedStacks: { [stackName: string]: cfninc.CfnIncludeProps; } = undefined;
    for (let i = 0; i < currentNestedStacks.length; i++) {
        const currentNestedStack = currentNestedStacks[i];

        const nestedStackResult = await cloudformation.describeStacks({
            StackName: currentNestedStack.stack.StackName
        }).promise();

        const nestedStack = nestedStackResult.Stacks.shift();
        const nestedStackTemplate = await cloudformation.getTemplate({
            StackName: nestedStack.StackName
        }).promise();

        const tmpFileName = tmp.tmpNameSync({
            postfix: '.yaml'
        });

        fs.writeFileSync(tmpFileName, nestedStackTemplate.TemplateBody);

        if (!nestedStacks) {
            nestedStacks = {};
        }
        const recursiveNestedStacks = await getNestedsStackDetails(nestedStack.StackName);
        nestedStacks[currentNestedStack.logicalResourceId] = {
            templateFile: tmpFileName,
            parameters: nestedStack.Parameters.reduce((previousValue, currentValue) => {
                previousValue[currentValue.ParameterKey] = currentValue.ParameterValue;
                return previousValue;
            }, {}),
            loadNestedStacks: recursiveNestedStacks
        }
    }

    return nestedStacks;
}

const getStackDetails = async (stackName: string) => {
    const cloudformation = new AWS.CloudFormation();
    const stacksResult = await cloudformation.describeStacks({
        StackName: stackName,
    }).promise();

    const stack = stacksResult.Stacks.shift();
    const stackTemplate = await cloudformation.getTemplate({
        StackName: stack.StackName,
    }).promise();

    const tmpFileName = tmp.tmpNameSync({
        postfix: '.yaml'
    });
    fs.writeFileSync(tmpFileName, stackTemplate.TemplateBody);

    const nestedsStacksDetails = await getNestedsStackDetails(stack.StackName)

    return {
        mainStack: {
            stack,
            tmpFileName
        },
        nestedsStacksDetails
    }
}

const main = async (rootStackName: string) => {
    const stackName = rootStackName;
    const stackDetails = await getStackDetails(stackName);

    const app = new App();
    const rootStack = new ReusableRootStackWithNestedStacks(app,
        stackName,
        stackDetails.mainStack.tmpFileName,
        stackDetails.nestedsStacksDetails,
        stackDetails.mainStack.stack
    );
    new OtherStack(rootStack, 'OtherStack')

    const deploy = new CdkDeployAbstraction({
        region: process.env.AWS_REGION
    });

    const deployResult = await deploy.deployCdkStack(app, rootStack);
    console.log(deployResult);
}

main('MyAwesomeRootStackName')
    .then(value => console.log('finish!'))
    .catch(err => console.log(err))

src/cdk/ReusableRootStackWithNestedStacks.ts

import * as AWS from 'aws-sdk';
import {Construct, Stack, StackProps} from "@aws-cdk/core";
import * as cfninc from '@aws-cdk/cloudformation-include';

export class ReusableRootStackWithNestedStacks extends Stack {
    constructor(scope: Construct, id: string, tmpFileName: string, nestedStacks: { [stackName: string]: cfninc.CfnIncludeProps; }, stack: AWS.CloudFormation.Stack, props?: StackProps) {
        super(scope, id, props);

        const template = new cfninc.CfnInclude(this, 'CfCurrentTemplate', {
            templateFile: tmpFileName,
            loadNestedStacks: nestedStacks,
            parameters: stack.Parameters.reduce((previousValue, currentValue) => {
                previousValue[currentValue.ParameterKey] = currentValue.ParameterValue;
                return previousValue;
            }, {})
        });
    }
}

src/cdk/OtherStack.ts:

import {Construct, NestedStack, NestedStackProps} from "@aws-cdk/core";
import * as iam from '@aws-cdk/aws-iam';

export class OtherStack extends NestedStack {
    constructor(scope: Construct, id: string, props?: NestedStackProps) {
        super(scope, id, props);

        new iam.ManagedPolicy(this, 'IamManagedPolicy', {
            managedPolicyName: 'TesteIAM-ManagedPolicy',
            document: new iam.PolicyDocument({
                statements: [
                    new iam.PolicyStatement({
                        effect: iam.Effect.ALLOW,
                        actions: [
                            'dynamodb:GetItem',
                            'dynamodb:Query',
                        ],
                        resources: ['*']
                    })
                ]
            }),
        })
    }
}

src/cdk/cdkDeployAbstraction:

import {Credentials} from "@aws-sdk/types";
import {CloudFormationDeployments} from "aws-cdk/lib/api/cloudformation-deployments";
import * as AWS from "aws-sdk";
import {App, Stack} from '@aws-cdk/core';
import {CloudFormationStackArtifact} from '@aws-cdk/cx-api';
import {DeployStackResult, SdkProvider} from "aws-cdk/lib";

export default class CdkDeployAbstraction {
    private readonly credentials: Credentials;
    private readonly region: string;

    constructor(config: { credentials?: Credentials; region: string }) {
        this.credentials = config.credentials;
        this.region = config.region;
    }

    public async deployCdkStack(app: App, stack: Stack, notificationTopicArn?: string): Promise<DeployStackResult> {
        const stackArtifact = app.synth().getStackByName(stack.stackName) as unknown as CloudFormationStackArtifact;
        const credentialProviderChain = new AWS.CredentialProviderChain();

        let credentials;
        if (this.credentials) {
            credentials = new AWS.Credentials({
                accessKeyId: this.credentials.accessKeyId,
                secretAccessKey: this.credentials.secretAccessKey,
            });

            credentialProviderChain.providers.push(credentials);
        }

        const sdkProvider = new SdkProvider(credentialProviderChain, this.region, {
            credentials: credentials,
        });

        const cloudFormation = new CloudFormationDeployments({sdkProvider});
        if (notificationTopicArn) {
            return cloudFormation.deployStack({
                // @ts-ignore
                stack: stackArtifact,
                notificationArns: [notificationTopicArn],
                quiet: true,
            });
        }
        return cloudFormation.deployStack({
            // @ts-ignore
            stack: stackArtifact,
            quiet: true,
        });
    }
}

Solution 2:[2]

So what I would do is:

1. Create root stack

Create a core root stack containing the common resources you want the customer stacks to use. You will need the arn/name of the resources you want to have access to in your customer stacks. One way is to use CfnOutput to write to the cdk.context.json file:

  // Example of outputting an S3 bucket
 const myBucket = new Bucket...
 new CfnOutput(this, "MyBucketARN", {
      value: String(myBucket.bucketARn)
    })

2. Create customer root stack

For each customer you would have something such as a customerRootStack which defines the root resources that you already created - since you don't want to overwrite/recreate you will want to create them using the existing ARN/name for example:

// Read the root stack context 
import rootData from '../commonApp/cdk.context.json`

s3.Bucket.fromBucketArn(this, 'MyBucket', rootData.nameOfRootStack.MyBucketARN)

// Define custom stuff for each customer
...

Note that it is possible to have multiple apps in same folder but you will need to ensure unique names etc) - probably easier to have a project per customer and one for root but that's up to you.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 renan.wao
Solution 2