【问题标题】:AWS CDK cross-stack references and deployment orderAWS CDK 跨堆栈参考和部署顺序
【发布时间】:2020-12-13 12:10:48
【问题描述】:

在 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 会尝试从我之前的集群定义中删除输出/导出,这显然会在部署时失败,因为它无法从集群堆栈中删除导出,直到有依赖它的服务。最后,我看到如下错误:

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 集群)并且部署失败。那么有没有办法克服呢?

【问题讨论】:

  • 您找到解决方法了吗?我也面临着类似的问题。
  • @kichik:我最终使用了相同的方法

标签: aws-cdk


【解决方案1】:

This 对我很有用。在我的情况下,我使用 CDK 并希望删除一个堆栈(比如说 stackA),其中的输出被引用为另一个堆栈中的输入(比如说 stackB 和 stackC)。因此,我打破了所有跨堆栈引用,将值手动放在 cloudFormation 模板上,然后将模板部署到每个堆栈(stackB 和 stackC)。然后我把CDK上的stackA去掉,部署成功了。

【讨论】:

    【解决方案2】:

    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
    

    它需要读取堆栈的权限,因此它可能不适用于跨账户部署。

    # 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()
    

    【讨论】:

    • 我们的部署有点复杂,在某些情况下,脚本逐步淘汰了导出,但链中的下一个堆栈部署失败。当整个处理被重试时,脚本试图删除导出但不能因为第二个堆栈没有完成更新。我最终使用cloudformation:ListImports 来确定是否使用了导出。我会在某个时候尝试在此处更新脚本,但可能需要一段时间。
    【解决方案3】:

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

    问题是,您的旧任务仍在运行,正如错误所告诉您的那样(SG 仍在使用中)。 此外,您是否正在尝试重用旧集群中的安全组?

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-05-08
      • 1970-01-01
      • 2020-03-05
      • 2020-08-12
      • 2022-01-03
      • 2022-01-20
      • 2021-02-18
      • 2018-12-13
      相关资源
      最近更新 更多