+135 410 16684Mon. - Fri. 10:00-22:00

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

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

如何实现对集群任务最小影响的 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 提供的无服务函数调用,如下图所示:

1-6

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 组实例选项卡:实例状态值应显示生命周期状态:

2-4

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

3-5

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

4-4

 

深入分析

我们来深入分析一下 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 开发人员指南 。