亚马逊AWS官方博客

如何实现对集群任务最小影响的 ECS 容器实例自动化终止方案

问题背景

Amazon ECS 是一种容器管理服务,可以很方便地运行、停止和管理群集上的 Docker 容器。当使用 ECS 运行容器任务时,会将它们放置在 ECS 群集上。Amazon ECS 从指定的映像存储库中,下载指定的容器映像,并在集群中的容器实例上运行这些映像所承载的任务。

我的同事 Chris Barclay 发了一篇很不错的博客文章,介绍了在 Auto Scaling 组缩小 ECS 集群之前,使用容器实例耗尽的方法,自动化地删除正在进行的容器实例。

根据多个实际的客户案例,需要从 Amazon ECS 群集中终止实例的应用场景很多且重要, 例如: EC2 AMI 的升级和更新,执行系统关键升级补丁,系统核心组库的更新,Docker 软件版本的升级和更新,ECS 代理的版本升级和更新,集群大小的变更等等。

解决方案

通常而言,这些应用场景,都有一个共同的目标就是当容器实例的终止时,或从集群中删除容器实例时,不会影响集群中正在进行的任务,也就是说,阻止将新任务安排在处于 DRAINING 状态的容器实例上,如果资源可用(或预先起动新的容器实例),则新任务分配到 ECS 集群中的其他容器实例,待终止的容器实例上正在运行的任务,等其成功迁移到其他容器实例后,终止实例。实战中,亦可手动修改容器实例的状态为 DRAINING。本文中,我们将展示如何实现对集群任务最小影响的 ECS 容器实例自动化终止方案,其中会需要使用Auto Scaling组的生命周期挂钩以及 Amazon Lambda 提供的无服务函数调用,如下图所示:

Auto Scaling 组支持可调用的生命周期挂钩,例如:Lambda 函数,以允许其在实例启动或终止之前完成,此例为实例终止之前。生命周期挂钩调用的 Lambda 函数完成以下两个任务:

  1. 将 ECS 容器实例状态设置为 DRAINING。
  2. 检查容器实例上是否存在任何正在进行的任务。 如有则会向 SNS 发布消息,再次调用该 Lambda 函数进行检查。

该 Lambda 函数会重复执行第2步,直到容器实例上没有任何正在运行的任务,或者生命周期挂钩心跳超时,以先发生者为准。 之后,控制权返回到 Auto Scaling 生命周期挂钩,终止容器实例。

参考示例

要实现上述自动化容器实例终止方案,可参考开源的 CloudFormation 模板,以及 S3 存储桶中上传 Lambda 部署软件包,设置本文中描述的资源。该模板创建以下资源:

  • VPC 和关联的网络元素(子网,安全组,路由表等)
  • ECS 群集,ECS 服务和示例 ECS 任务定义
  • 具有两个 EC2 实例和包含生命周期终止挂钩的 Auto Scaling 组
  • Lambda 函数
  • SNS 话题
  • 能执行 Lambda 函数的 IAM 角色

鉴于中国区有关 Auto Scaling 组的可信任实体和全球的命名方式有所区别,因此可参考这里的修改方法,对 CloudFormation 模板进行配置和更改。

创建 CloudFormation 堆栈,我们可以通过触发实例终止事件,来了解这是如何工作的:

  • 在 Amazon EC2 控制台中 ,选择 Auto Scaling Groups 并选择由 CloudFormation 创建的 Auto Scaling 组的名称。
  • 选择操作 , 编辑并更新服务,将实例的数量减少1个。

这将触发一个实例的终止过程。选择 Auto Scaling 组实例选项卡:实例状态值应显示生命周期状态:

此时,生命周期挂钩被激活并向 SNS 发布消息,最终触发 Lambda 函数的响应和执行。之后, Lambda 函数将 ECS 容器实例状态更改为 DRAINING。ECS 服务介入调度,停止实例上的任务并在可用实例上启动该任务。

任务完成后,Auto Scaling 组活动历史记录确认 EC2 实例已终止。

 

深入分析

我们来深入分析一下 Lambda 函数内部的工作原理。该函数首先检查,收到的事件中的 LifecycleTransition 值是否为 EC2_INSTANCE_TERMINATING,表示当前已经进入生命周期挂钩的终止状态之前。

 # If the event received is instance terminating...
if 'LifecycleTransition' in message.keys():
    print("message autoscaling {}".format(message['LifecycleTransition']))
if message['LifecycleTransition'].find('autoscaling:EC2_INSTANCE_TERMINATING') > -1:

继续调用函数 “checkContainerInstanceTaskStatus”。该函数根据容器实例的 ID,将容器实例状态设置为 ‘DRAINING’。

# Get lifecycle hook name
lifecycleHookName = message['LifecycleHookName']
print("Setting lifecycle hook name {} ".format(lifecycleHookName))

# Check if there are any tasks running on the instance
tasksRunning = checkContainerInstanceTaskStatus(Ec2InstanceId)

然后,检查实例上是否有任务正在运行。如有任务正在运行,则向 SNS 发布消息以再次触发 Lambda 函数后退出。

# Use Task ARNs to get describe tasks
descTaskResp = ecsClient.describe_tasks(cluster=clusterName, tasks=listTaskResp['taskArns'])
for key in descTaskResp['tasks']:
 print("Task status {}".format(key['lastStatus']))
 print("Container instance ARN {}".format(key['containerInstanceArn']))
 print("Task ARN {}".format(key['taskArn']))

# Check if any tasks are running
if len(descTaskResp['tasks']) > 0:
 print("Tasks are still running..")
 return 1
else:
 print("NO tasks are on this instance {}..".format(Ec2InstanceId))
 return 0

继续执行的 Lambda 函数,发现容器实例上没有运行的任务时,则继续完成生命周期挂钩并终止 EC2 实例。

#Complete lifecycle hook.
try:
 response = asgClient.complete_lifecycle_action(
 LifecycleHookName=lifecycleHookName,
 AutoScalingGroupName=asgGroupName,
 LifecycleActionResult='CONTINUE',
 InstanceId=Ec2InstanceId)
 print("Response = {}".format(response))
 print("Completedlifecycle hook action")
except Exception, e:
 print(str(e)) 

结论

本文讨论了 ECS 容器实例终止的多种应用场景,提供了对集群任务最小影响的 ECS 容器实例自动化终止方案,并通过参考示例展示和深入分析了其工作原理。基于参考示例,可以使用 CloudFormation,Lambda 等服务,实现真正的滚动部署 ,先启动新实例并批量终止实例,同时保证对现有的集群任务带来最小影响。要了解有关容器实例耗尽的更多信息,请参阅 Amazon ECS 开发人员指南 。

 

黄帅(Henry Huang)

亚马逊 AWS 专业服务团队 DevOps 咨询顾问。负责企业级客户的云架构设计和优化、DevOps 组织咨询和技术实施。在软件研发领域有多年架构设计和运维、团队管理经验,对 DevOps、云原生微服务治理框架、容器化平台运维等有深入的研究和热情。