如何将更新后的 Docker 映像部署到 Amazon ECS 任务?

什么是正确的方法,使我的 亚马逊 ECS任务更新他们的 Docker 图像,一旦说图像已经在相应的注册表更新?

110335 次浏览

每次启动任务(通过 StartTaskRunTask API 调用或作为服务的一部分自动启动) ,ECS 代理将执行任务定义中指定的 imagedocker pull。如果每次推送到注册表时使用相同的映像名称(包括标记) ,则应该能够通过运行新任务来运行新映像。请注意,如果 Docker 因为任何原因无法到达注册中心(例如,网络问题或身份验证问题) ,ECS 代理将尝试使用一个缓存的图像; 如果您想避免在更新图像时使用缓存的图像,您将希望每次向注册中心推送一个不同的标记,并在运行新任务之前相应地更新任务定义。

更新: 现在可以通过设置在 ECS 代理上的 ECS_IMAGE_PULL_BEHAVIOR环境变量来调整这种行为。详情请参阅 文件。在撰写本文时,支持以下设置:

用于为容器实例自定义拉图过程的行为:

  • 如果指定了 default,则远程提取图像。如果映像提取失败,那么容器将在实例上使用缓存的映像。

  • 如果指定了 always,则始终远程提取图像。如果图像拉取失败,则任务失败。此选项确保始终提取图像的最新版本。任何缓存的图像都会被忽略,并受到自动图像清理过程的影响。

  • 如果指定了 once,则只有在以前的任务没有在同一容器实例上提取该映像,或者缓存的映像被自动映像清理过程删除时,才会远程提取该映像。否则,将使用实例上的缓存映像。这样可以确保不会尝试不必要的图像拉动。

  • 如果指定了 prefer-cached,则在没有缓存图像的情况下远程提取图像。否则,将使用实例上的缓存映像。禁用容器的自动图像清理,以确保不删除缓存的图像。

我创建了 剧本,用于将更新后的 Docker 映像部署到 ECS 上的临时服务,以便相应的任务定义引用 Docker 映像的当前版本。我不确定自己是否遵循了最佳实践,因此欢迎反馈。

为了让脚本工作,您需要一个空闲的 ECS 实例或者一个 deploymentConfiguration.minimumHealthyPercent值,以便 ECS 可以窃取一个实例,将更新后的任务定义部署到。

我的算法是这样的:

  1. 使用 Git 修订版对任务定义中与容器对应的 Docker 图像进行标记。
  2. 将 Docker 图像标记推送到相应的注册中心。
  3. 在任务定义家族中注销旧任务定义。
  4. 注册新的任务定义,现在引用带有当前 Git 修订版标记的 Docker 图像。
  5. 更新服务以使用新的任务定义。

我的代码粘贴如下:

部署电子系统

#!/usr/bin/env python3
import subprocess
import sys
import os.path
import json
import re
import argparse
import tempfile


_root_dir = os.path.abspath(os.path.normpath(os.path.dirname(__file__)))
sys.path.insert(0, _root_dir)
from _common import *




def _run_ecs_command(args):
run_command(['aws', 'ecs', ] + args)




def _get_ecs_output(args):
return json.loads(run_command(['aws', 'ecs', ] + args, return_stdout=True))




def _tag_image(tag, qualified_image_name, purge):
log_info('Tagging image \'{}\' as \'{}\'...'.format(
qualified_image_name, tag))
log_info('Pulling image from registry in order to tag...')
run_command(
['docker', 'pull', qualified_image_name], capture_stdout=False)
run_command(['docker', 'tag', '-f', qualified_image_name, '{}:{}'.format(
qualified_image_name, tag), ])
log_info('Pushing image tag to registry...')
run_command(['docker', 'push', '{}:{}'.format(
qualified_image_name, tag), ], capture_stdout=False)
if purge:
log_info('Deleting pulled image...')
run_command(
['docker', 'rmi', '{}:latest'.format(qualified_image_name), ])
run_command(
['docker', 'rmi', '{}:{}'.format(qualified_image_name, tag), ])




def _register_task_definition(task_definition_fpath, purge):
with open(task_definition_fpath, 'rt') as f:
task_definition = json.loads(f.read())


task_family = task_definition['family']


tag = run_command([
'git', 'rev-parse', '--short', 'HEAD', ], return_stdout=True).strip()
for container_def in task_definition['containerDefinitions']:
image_name = container_def['image']
_tag_image(tag, image_name, purge)
container_def['image'] = '{}:{}'.format(image_name, tag)


log_info('Finding existing task definitions of family \'{}\'...'.format(
task_family
))
existing_task_definitions = _get_ecs_output(['list-task-definitions', ])[
'taskDefinitionArns']
for existing_task_definition in [
td for td in existing_task_definitions if re.match(
r'arn:aws:ecs+:[^:]+:[^:]+:task-definition/{}:\d+'.format(
task_family),
td)]:
log_info('Deregistering task definition \'{}\'...'.format(
existing_task_definition))
_run_ecs_command([
'deregister-task-definition', '--task-definition',
existing_task_definition, ])


with tempfile.NamedTemporaryFile(mode='wt', suffix='.json') as f:
task_def_str = json.dumps(task_definition)
f.write(task_def_str)
f.flush()
log_info('Registering task definition...')
result = _get_ecs_output([
'register-task-definition',
'--cli-input-json', 'file://{}'.format(f.name),
])


return '{}:{}'.format(task_family, result['taskDefinition']['revision'])




def _update_service(service_fpath, task_def_name):
with open(service_fpath, 'rt') as f:
service_config = json.loads(f.read())
services = _get_ecs_output(['list-services', ])[
'serviceArns']
for service in [s for s in services if re.match(
r'arn:aws:ecs:[^:]+:[^:]+:service/{}'.format(
service_config['serviceName']),
s
)]:
log_info('Updating service with new task definition...')
_run_ecs_command([
'update-service', '--service', service,
'--task-definition', task_def_name,
])




parser = argparse.ArgumentParser(
description="""Deploy latest Docker image to staging server.
The task definition file is used as the task definition, whereas
the service file is used to configure the service.
""")
parser.add_argument(
'task_definition_file', help='Your task definition JSON file')
parser.add_argument('service_file', help='Your service JSON file')
parser.add_argument(
'--purge_image', action='store_true', default=False,
help='Purge Docker image after tagging?')
args = parser.parse_args()


task_definition_file = os.path.abspath(args.task_definition_file)
service_file = os.path.abspath(args.service_file)


os.chdir(_root_dir)


task_def_name = _register_task_definition(
task_definition_file, args.purge_image)
_update_service(service_file, task_def_name)

好吧

import sys
import subprocess




__all__ = ['log_info', 'handle_error', 'run_command', ]




def log_info(msg):
sys.stdout.write('* {}\n'.format(msg))
sys.stdout.flush()




def handle_error(msg):
sys.stderr.write('* {}\n'.format(msg))
sys.exit(1)




def run_command(
command, ignore_error=False, return_stdout=False, capture_stdout=True):
if not isinstance(command, (list, tuple)):
command = [command, ]
command_str = ' '.join(command)
log_info('Running command {}'.format(command_str))
try:
if capture_stdout:
stdout = subprocess.check_output(command)
else:
subprocess.check_call(command)
stdout = None
except subprocess.CalledProcessError as err:
if not ignore_error:
handle_error('Command failed: {}'.format(err))
else:
return stdout.decode() if return_stdout else None

注册新任务定义并更新服务以使用新任务定义是 AWS 推荐的方法。最简单的方法是:

  1. 导航到任务定义
  2. 选择正确的任务
  3. 选择创建新版本
  4. 如果您已经使用类似于: last 标记的内容提取容器映像的最新版本,那么只需单击 Create。否则,请更新容器映像的版本号,然后单击“创建”。
  5. 扩大行动
  6. 选择“更新服务”(两次)
  7. 然后等待服务重新启动

本教程 有更多细节,并描述了上述步骤如何适应端到端产品开发过程。

充分披露: 这个教程的特点是来自 Bitnami 的容器,我为 Bitnami 工作。然而,这里表达的想法是我个人的,而不是 Bitnami 的观点。

如果任务在服务下运行,则可以强制执行新的部署。这将强制重新计算任务定义并提取新的容器映像。

aws ecs update-service --cluster <cluster name> --service <service name> --force-new-deployment

以下命令对我有效

docker build -t <repo> .
docker push <repo>
ecs-cli compose stop
ecs-cli compose start

使用 AWS cli 我尝试了上面建议的 AWS ecs update-service。没有接到 ECR 最新的码头工人。最后,我重新运行了创建 ECS 集群的 Anble 剧本。当 ecs _ taskDefinition 运行时,任务定义的版本会发生碰撞。那就没事了。收到新的码头图像。

实际上,不确定任务版本的更改是否强制重新部署,或者使用 ecs _ service 的剧本是否导致任务重新加载。

如果有人感兴趣,我会得到许可,发布我的剧本的净化版本。

AWS 代码管道。

您可以将 ECR 设置为源,将 ECS 设置为要部署到的目标。

嗯,我也试图找到一个自动化的方式做到这一点,这是推的变化,ECR,然后最新的标签应该由服务提取。 您可以通过从集群中停止服务的任务手动执行此操作。新任务将提取更新的 ECR 容器。

有两种方法可以做到这一点。

首先,使用 AWS CodeDeploy。您可以在 ECS 服务定义中配置 Blue/Green 部署节。这包括一个 CodeDeployRoleForECS、用于开关的另一个 TargetGroup 和一个测试监听器(可选)。AWS ECS 将创建 CodeDeploy 应用程序和部署组,并为您将这些 CodeDeploy 资源与 ECS Cluster/Service 和 ELB/TargetGroups 链接起来。然后可以使用 CodeDeploy 启动部署,在部署中需要输入 AppSpec,该 AppSpec 指定使用什么任务/容器来更新什么服务。下面是指定新任务/容器的地方。然后,您将看到新的 TargetGroup 中的新实例被旋转起来,旧的 TargetGroup 与 ELB 断开连接,并且很快注册到旧 TargetGroup 的旧实例将被终止。

听起来很复杂。实际上,既然/如果您已经在 ECS 服务上启用了自动伸缩,那么一个简单的方法就是使用控制台或 cli 强制执行一个新的部署,就像这里的一位先生指出的那样:

aws ecs update-service --cluster <cluster name> --service <service name> --force-new-deployment

通过这种方式,您仍然可以使用“滚动更新”部署类型,并且如果一切正常,ECS 将简单地旋转新实例并排出旧实例,而不会停机。不好的一面是您失去了对部署的良好控制,如果出现错误,您不能回滚到以前的版本,这将中断正在进行的服务。但这是一个非常简单的方法。

顺便说一句,不要忘记为最小健康百分比和最大百分比设置合适的数字,比如100和200。

如果 docker 图像标签是相同的,下面的方法对我很有用:

  1. 进入群集和服务。
  2. 选择“服务”并单击“更新”。
  3. 将任务数设置为0并更新。
  4. 部署完成后,将任务数量重新缩放为1。

以下 API 同样有效:

aws ecs update-service --cluster <cluster_name> --service <service_name> --force-new-deployment

经过几个小时的努力,我们总结出了自动展开更新图像的简化步骤:

1.ECS 任务定义更改: 为了更好地理解,让我们假设您已经创建了一个任务定义,其详细信息如下(注意: 这些数字将根据您的任务定义相应地更改) :

launch_type = EC2


desired_count = 1

那么你需要做出以下改变:

deployment_minimum_healthy_percent = 0  //this does the trick, if not set to zero the force deployment wont happen as ECS won't allow to stop the current running task


deployment_maximum_percent = 200  //for allowing rolling update

2. 将你的图像标记为 < Your-image-name > : last。最新的按键就可以了 被各自的 ECS 任务拖动。

sudo docker build -t imageX:master .   //build your image with some tag
sudo -s eval $(aws ecr get-login --no-include-email --region us-east-1)  //login to ECR
sudo docker tag imageX:master <your_account_id>.dkr.ecr.us-east-1.amazonaws.com/<your-image-name>:latest    //tag your image with latest tag

3. 将图像按到 ECR

sudo docker push  <your_account_id>.dkr.ecr.us-east-1.amazonaws.com/<your-image-name>:latest

4. 应用 部队部署

sudo aws ecs update-service --cluster <your-cluster-name> --service <your-service-name> --force-new-deployment --region us-east-1

注意: 我已经编写了假设该区域为 Us-east-1的所有命令。在实现时,只需将其替换为您各自的区域。

因为在 AWS 方面没有任何进展。我将给出一个简单的 python 脚本,它确切地执行在 迪马Samuel Karp的高评分答案中描述的步骤。

首先将您的映像推入 AWS 注册表 ECR,然后运行脚本:

import boto3, time


client = boto3.client('ecs')
cluster_name = "Example_Cluster"
service_name = "Example-service"
reason_to_stop = "obsolete deployment"


# Create new deployment; ECS Service forces to pull from docker registry, creates new task in service
response = client.update_service(cluster=cluster_name, service=service_name, forceNewDeployment=True)


# Wait for ecs agent to start new task
time.sleep(10)


# Get all Service Tasks
service_tasks = client.list_tasks(cluster=cluster_name, serviceName=service_name)


# Get meta data for all Service Tasks
task_meta_data = client.describe_tasks(cluster=cluster_name, tasks=service_tasks["taskArns"])


# Extract creation date
service_tasks = [(task_data['taskArn'], task_data['createdAt']) for task_data in task_meta_data["tasks"]]


# Sort according to creation date
service_tasks = sorted(service_tasks, key= lambda task: task[1])


# Get obsolete task arn
obsolete_task_arn = service_tasks[0][0]
print("stop ", obsolete_task_arn)


# Stop obsolete task
stop_response = client.stop_task(cluster=cluster_name, task=obsolete_task_arn, reason=reason_to_stop)

这个代码的作用是:

  1. 使用服务中的新映像创建一个新任务
  2. 使用服务中的旧映像停止过时的旧任务

如果您使用任何 IAC 工具来设置您的 ECS 任务,比如 terraform,那么您总是可以在任务定义中更新图像版本。 Terraform 将基本上取代旧的任务定义并创建新的任务定义,ECS 服务将开始使用新的任务定义和更新的映像。

另一种方法是始终有 更新命令在您的管道,建立您的映像将用于 ECS 任务,一旦您建立的映像-只是做一个力量部署。

aws ecs update-service --cluster clusterName --service serviceName --force-new-deployment