在其他 App 项目中重用父 CDK Stack

Reuse a parent CDK Stack in other App project

首先,重要的是我没有像往常一样使用 CDK。相反,我正在以编程方式即时创建资源。所以,基本上,我有一个多租户应用程序,它在机上创建了一个客户根堆栈,在客户生命周期内将包含带有资源的嵌套堆栈。

第一次执行的代码是这样的:

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

我现在想在其他例程中创建的其他堆栈上重用根堆栈 Customer-123-RootStack。例如,客户将在我们的平台中创建其他 AWS 资源,例如 AWS EventBridge 规则或 ACM 证书。

如果对另一个嵌套栈执行相同的代码,第一个嵌套栈将被删除。

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

我已阅读此文档,但无法弄清楚该怎么做:https://docs.aws.amazon.com/cdk/v2/guide/resources.html#resources_importing

我可以使用 SDK 获取堆栈,但不明白如何让它与 CDK 一起工作。

编辑:为了让我的问题更清楚:我需要将父现有堆栈(由其他应用程序创建,在其他时刻,在其他代码库中创建)引用到新的嵌套堆栈。

所以我会做的是:

1。创建根堆栈

创建一个核心根堆栈,其中包含您希望客户堆栈使用的公共资源。您将需要 arn/name 您希望在客户堆栈中访问的资源。一种方法是使用 CfnOutput 写入 cdk.context.json 文件:

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

2。创建客户根堆栈

对于每个客户,您将拥有诸如 customerRootStack 之类的东西,它定义了您已经创建的根资源 - 因为您不想 overwrite/recreate 您将希望使用现有的 ARN/name 例如:

// 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
...

请注意,同一文件夹中可以有多个应用程序,但您需要确保唯一的名称等)- 每个客户一个项目和根用户一个项目可能更容易,但这取决于您。

我找到了一个解决方案并创建了这个示例存储库:

我混合了 AWS SDK CloudFormation 模块:

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,
        });
    }
}