如何为CDK管理的EC2实例启用终止保护并允许CDK替换时终止?
解决CDK管理的EC2堡垒机终止保护导致更新失败的问题
你的核心问题是:直接给BastionHostLinux对应的EC2实例开启DisableApiTermination=true后,CloudFormation在更新堆栈、销毁旧实例时会因终止保护无法执行操作。以下是可行的解决方案:
方案:自定义资源 + Lambda 钩子
通过创建CloudFormation自定义资源,关联Lambda函数,在实例被标记为删除时自动关闭终止保护,确保CloudFormation能正常销毁旧实例。
步骤1:编写Lambda函数(自动关闭终止保护)
Lambda函数接收CloudFormation事件,当触发删除操作时,调用EC2 API修改实例属性,关闭终止保护。这里用Python编写Lambda代码(简洁易维护):
import boto3 import cfnresponse ec2 = boto3.client('ec2') def handler(event, context): try: instance_id = event['ResourceProperties']['InstanceId'] if event['RequestType'] == 'Delete': # 关闭实例终止保护 ec2.modify_instance_attribute( InstanceId=instance_id, DisableApiTermination={'Value': False} ) cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) except Exception as e: cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': str(e)})
步骤2:在CDK中集成自定义资源
在你的Java CDK代码中,创建Lambda函数、配置权限,并关联自定义资源与堡垒机实例:
import software.amazon.awscdk.*; import software.amazon.awscdk.services.lambda.*; import software.amazon.awscdk.services.lambda.Runtime; import software.amazon.awscdk.services.ec2.*; import software.amazon.awscdk.services.cloudformation.*; import software.constructs.Construct; import java.util.List; import java.util.Map; public class BastionStack extends Stack { public BastionStack(final Construct scope, final String id) { this(scope, id, null); } public BastionStack(final Construct scope, final String id, final StackProps props) { super(scope, id, props); // 创建堡垒机实例 BastionHostLinux bastion = BastionHostLinux.Builder.create(this, "BastionHost") .vpc(Vpc.fromLookup(this, "Vpc", VpcLookupOptions.builder().isDefault(true).build())) .build(); // 获取底层CfnInstance资源 CfnInstance cfnBastion = (CfnInstance) bastion.getNode().getDefaultChild().getNode().getDefaultChild(); // 开启终止保护 cfnBastion.setDisableApiTermination(true); // 创建Lambda函数:销毁前关闭终止保护 Function disableTerminationLambda = Function.Builder.create(this, "DisableTerminationLambda") .runtime(Runtime.PYTHON_3_9) .code(Code.fromInline(""" import boto3 import cfnresponse ec2 = boto3.client('ec2') def handler(event, context): try: instance_id = event['ResourceProperties']['InstanceId'] if event['RequestType'] == 'Delete': ec2.modify_instance_attribute( InstanceId=instance_id, DisableApiTermination={'Value': False} ) cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) except Exception as e: cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': str(e)}) """)) .handler("index.handler") .build(); // 给Lambda添加修改EC2实例属性的权限 disableTerminationLambda.addToRolePolicy(PolicyStatement.Builder.create() .actions(List.of("ec2:ModifyInstanceAttribute")) .resources(List.of(String.format("arn:aws:ec2:%s:%s:instance/%s", this.getRegion(), this.getAccount(), cfnBastion.getRef()))) .build()); // 创建自定义资源,关联堡垒机实例 CustomResource.Builder.create(this, "DisableTerminationHook") .serviceToken(disableTerminationLambda.getFunctionArn()) .properties(Map.of("InstanceId", cfnBastion.getRef())) .dependsOn(bastion) // 确保自定义资源在堡垒机销毁前执行 .build(); } }
方案说明
- 自定义资源会绑定堡垒机实例的生命周期:当CloudFormation执行销毁旧实例的操作时,会先触发Lambda函数关闭终止保护,再执行销毁。
- 既满足渗透测试要求(实例运行时始终开启终止保护),又不会阻碍CDK/CloudFormation的正常更新流程。
替代方案:使用EC2启动模板(可选)
如果你的BastionHostLinux支持指定启动模板,可以在启动模板中配置DisableApiTermination=true,然后结合上述自定义资源方案,同样能解决更新时的销毁问题。不过此方案需要确保启动模板的配置与CDK代码兼容。
内容的提问来源于stack exchange,提问作者Robert Elliot
相关产品推荐
相关产品推荐

