'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 |