AWS CDK 跨堆栈参考和部署顺序

AWS CDK cross-stack references and deployment order

AWS CDK中循环依赖问题的解决方法是什么: 假设我有一个用于 ECS 集群的堆栈和一个用于 ECS 服务的堆栈(其中几个):

export class EcsClusterStack extends cdk.Stack {
    public readonly cluster: ecs.Cluster
    ...
}

export class EcsServiceStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, cluster: ecs.ICluster) { }
}

现在,我可以编写我的应用程序了:

const app = new cdk.App();
const vpc = new VpcStack(app,  'vpc');
const cluster = new ClusterStack(app, 'ecs', vpc.vpc);

const service = new EcsServiceStack(app, 'ecs-service', cluster.cluster);

假设在那之后,我想将我的 ECS 服务从一个集群迁移到另一个集群。我会创建另一个 ECS 集群堆栈并将其传递给 ECS 服务,但问题是: AWS CDK 自动生成输出(在集群堆栈中有集群名称等输出),然后,当我想将我的 ECS 服务迁移到另一个集群时,如果我将另一个 ICluster 对象传递给 ECS 服务堆栈构造函数,AWS CDK 会尝试从我之前的集群定义中删除 Outputs/Exports ,这显然会在部署时失败,因为它无法从集群堆栈中删除导出,直到有依赖它的服务为止。最后,我看到如下错误:

0 | 7:15:19 PM | UPDATE_IN_PROGRESS   | AWS::CloudFormation::Stack            | ecs User Initiated
0 | 7:15:26 PM | UPDATE_ROLLBACK_IN_P | AWS::CloudFormation::Stack            | ecs Export ecs:ExportsOutputFnGetAttdefaultasgspotInstanceSecurityGroup2D2AFE98GroupId1084B7B2 cannot be deleted as it is in use by ecs-service

如果有一种方法可以强制先部署 ECS 服务堆栈,那将解决问题,但似乎 AWS CDK 总是先部署依赖项(在我的例子中是 ECS 集群)并且部署失败。那么有没有办法克服这个问题呢?

我是这样理解你的问题的:创建另一个集群,将 TaskDefinition with Service 从旧集群迁移到新集群。

问题是,您的旧任务仍在 运行ning,因为错误告诉您(SG 仍在使用中)。 此外,您是否正在尝试 re-use 旧集群中的安全组?

如果不是这样,那么您需要实例化一个 new EcsServiceStack,但要使用新的集群参数。 或者,如果您不关心“手动 blue/green 部署”,那么您可以销毁 old EcsServiceStack。 然后重新运行修改代码的CDK命令应该运行.

AWS 添加了官方 workaround。它允许您在依赖堆栈中手动创建导出。当您需要删除导出时,手动创建它,删除它的使用,部署,删除它,然后再次部署。

this.exportAttribute(this.bucket.bucketName)

我发现的一种解决方法是强制 CDK 将部署拆分为两个步骤。首先,我部署了使用导出的堆栈,因此它不再使用它。然后,我部署创建导出的堆栈,以便在导出不再使用后将其删除。即使您在命令中指定堆栈名称,它仍会部署它所依赖的所有堆栈。所以我必须使用 --exclusively 标志。

cdk deploy --exclusively ecs-service
cdk deploy

在您的情况下,您需要在创建新集群和部署堆栈之前执行所有步骤,以便您可以在 ecs-service.

中导入一些新内容

GitHub 上有一个 open issue


我创建了以下脚本来帮助自动化该过程。它逐步淘汰了两个部署中的出口。在第一次部署时,它会恢复所有已删除的导出,但将它们标记为要删除。这允许第一个部署安全地删除它的使用。在第二次部署中,脚本实际上在没有其他堆栈使用它们之后删除了导出。

要使用脚本,您必须将合成和部署步骤分开,运行 它们之间的脚本。

cdk synth && python phase-out-ref-exports.py && cdk deploy --app cdk.out --all

它需要读取堆栈的权限,因此它可能不适用于 cross-account 部署。

# phase-out-ref-exports.py
import json
import os
import os.path
 
from aws_cdk import cx_api
import boto3
import botocore.exceptions
 
 
def handle_template(stack_name, account, region, template_path):
    # get outputs from existing stack (if it exists)
    try:
        # TODO handle different accounts
        print(f"Checking exports of {stack_name}...")
        stack = boto3.client("cloudformation", region_name=region).describe_stacks(StackName=stack_name)["Stacks"][0]
        old_outputs = {
            o["OutputKey"]: o
            for o in stack.get("Outputs", [])
        }
    except botocore.exceptions.ClientError as e:
        print(f"Unable to phase out exports for {stack_name} on {account}/{region}: {e}")
        return
 
    # load new template generated by CDK
    new_template = json.load(open(template_path))
    if "Outputs" not in new_template:
        new_template["Outputs"] = {}
 
    # get output names for both existing and new templates
    new_output_names = set(new_template["Outputs"].keys())
    old_output_names = set(old_outputs.keys())
 
    # phase out outputs that are in old template but not in new template
    for output_to_phase_out in old_output_names - new_output_names:
        # if we already marked it for removal last deploy, remove the output
        if old_outputs[output_to_phase_out].get("Description") == "REMOVE ON NEXT DEPLOY":
            print(f"Removing {output_to_phase_out}")
            continue
 
        if not old_outputs[output_to_phase_out].get("ExportName"):
            print(f"This is an export with no name, ignoring {old_outputs[output_to_phase_out]}")
            continue
 
        # add back removed outputs
        print(f"Re-adding {output_to_phase_out}, but removing on next deploy")
        new_template["Outputs"][output_to_phase_out] = {
            "Value": old_outputs[output_to_phase_out]["OutputValue"],
            "Export": {
                "Name": old_outputs[output_to_phase_out]["ExportName"]
            },
            # mark for removal on next deploy
            "Description": "REMOVE ON NEXT DEPLOY",
        }
 
    # replace template
    json.dump(new_template, open(template_path, "w"), indent=4)
 
 
def handle_assembly(assembly):
    for s in assembly.stacks:
        handle_template(s.stack_name, s.environment.account, s.environment.region, s.template_full_path)
 
    for a in assembly.nested_assemblies:
        handle_assembly(a.nested_assembly)
 
 
def main():
    assembly = cx_api.CloudAssembly("cdk.out")
    handle_assembly(assembly)
 

if __name__ == "__main__":
    main()

This 对我很有用。在我的例子中,我使用的是 CDK 并想删除一个堆栈(比如说 stackA),输出在另一个堆栈(比如说 stackB 和 stackC)中被引用为输入。所以我打破了所有交叉堆栈引用,将值手动放在 cloudFormation 模板上,然后我将模板部署到每个堆栈(stackB 和 stackC)。然后我把CDK上的stackA去掉,部署成功。