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


110

一旦在相应的注册表中更新了映像后,使我的Amazon ECS任务更新其Docker映像的正确方法是什么?


我建议运行自动/预定的Lambda函数。这样,它在实例外部。你有尝试过吗?您也可以使用SWF一次执行步骤
-iSkore

我不需要自动化它@iSkore。我想最终为其编写一个脚本,但是选择自己何时运行它。
aknuds1 2016年

啊,哎呀。不确定。您能否提供更多信息?
iSkore

@iSkore我不知道如何比我已经描述的更好。过程是:1.将新版本的Docker映像推送到注册表。2.将新映像版本部署到ECS。问题是如何实施后者。
aknuds1年

这对于EKS来说也不是容易或不明显的。F是使用群集,部署新映像的最常见任务,因此文档中不清楚吗?

Answers:


88

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

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

1
我认为要使其正常工作,您需要确保ECS实例上有足够的资源来部署相同大小的其他任务。我假设AWS实际上尝试执行热交换,等待新任务实例被预引导,然后终止旧任务。如果不这样做,它将仅添加具有0个正在运行的实例的“部署”条目。
Alex Fedulov '18

3
@AlexFedulov,是的,我认为您是正确的。为了在创建新部署时不会造成停机,您可以:1)设置足够的实例以将新版本与旧版本一起部署。这可以通过自动缩放来实现。2)使用Fargate部署类型。通过将服务的“最低健康百分比”参数设置为0,以允许ECS在部署新服务之前删除旧服务,可以避免分配额外的资源。但是,这将导致一些停机时间。
迪马

3
未知选项:
force

1
未知选项:--force-new-deployment:升级awscli
Kyle Parisi

1
我试过此命令,它不使用新图像更新容器,它旋转具有相同旧图像的另一个容器。所以,我最终即使在服务我已经代表特定的所需数量= 1个运行两个容器
数学

61

每次启动任务(通过StartTaskRunTaskAPI调用或者是作为服务的一部分自动启动)时,ECS代理将执行docker pullimage你设置的任务定义。如果每次推送到注册表时都使用相同的映像名称(包括标签),则应该能够通过运行新任务来运行新映像。请注意,如果Docker由于任何原因(例如,网络问题或身份验证问题)无法到达注册表,则ECS代理将尝试使用缓存的映像;如果要避免在更新映像时使用缓存的映像,则需要在运行新任务之前每次将不同的标记推送到注册表中并相应地更新任务定义。

更新:现在可以通过ECS_IMAGE_PULL_BEHAVIOR在ECS代理上设置的环境变量来调整此行为。有关详细信息,请参见文档。截至撰写本文时,支持以下设置:

用于自定义容器实例的拉取映像过程的行为。下面介绍了可选的行为:

  • 如果default指定,则远程拖动图像。如果图像拉取失败,则容器将在实例上使用缓存的图像。

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

  • 如果once指定,则仅当同一容器实例上的先前任务尚未将其拖出图像或通过自动图像清理过程删除了高速缓存的图像时,才远程拖动该图像。否则,将使用实例上的缓存图像。这样可以确保不会尝试不必要的图像拖拽。

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


4
你确定吗?我曾见过实例,即使我将新映像推送到Dockerhub(使用相同的标记名)后,旧Docker映像仍会运行。我想也许我应该在每次构建新图像时都更改标签名称。但是,这在我的经历中很少见,因此也许只是暂时的网络问题。(我知道您在ECS上工作,所以您是回答此问题的最佳人选,但这并不是我所经历的。抱歉,这是不礼貌的,不是我的意图!)
Ibrahim

1
是的,当前行为是每次都会尝试拉动。如果拉取失败(网络问题,权限不足等),它将尝试使用缓存的图像。您可以在通常位于中的代理日志文件中找到更多详细信息/var/log/ecs
塞缪尔·卡普

26

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

  1. 导航到任务定义
  2. 选择正确的任务
  3. 选择创建新修订
  4. 如果您已经使用:latest标记来获取容器映像的最新版本,则只需单击创建。否则,更新容器映像的版本号,然后单击“创建”。
  5. 展开动作
  6. 选择更新服务(两次)
  7. 然后等待服务重新启动

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

完全公开:本教程介绍了Bitnami的容器,我为Bitnami工作。但是这里表达的想法是我自己的,而不是Bitnami的观点。


3
这可行,但是您可能必须更改服务的最小/最大值。如果只有一个EC2实例,则必须将最小正常运行百分比设置为零,否则它将永远不会终止任务(使您的服务暂时脱机)以部署更新的容器。
Malvineous

3
@Malvineous好点!在本教程的“ ECS设置”部分中,我对此进行了精确描述。下面是该部分的推荐配置:任务数- 1,最少百分之健康- 0,最大百分比- 200
尼尔

@尼尔我尝试了这里所说的方法...仍然没有喜悦
哈菲兹

@Hafiz如果您需要帮助以解决此问题,则应描述获得的距离以及遇到的错误。
Neal 2015年

这仅适用于服务,不适用于没有服务的任务。
zaitsman

8

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

首先,使用AWS CodeDeploy。您可以在ECS服务定义中配置“蓝色/绿色部署”部分。这包括CodeDeployRoleForECS,用于交换的另一个TargetGroup和测试侦听器(可选)。AWS ECS将创建CodeDeploy应用程序和部署组,并将这些CodeDeploy资源与您的ECS集群/服务和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。


有没有一种方法而不必更改IP?在我的网站上,它可以运行,但是它改变了我正在运行的私有IP
Migdotcom

@Migdotcom当需要代理NLB时,我遇到了类似的问题。简而言之,使EC2实例IP保持相同的唯一方法是使用弹性IP地址或使用其他方法。我不知道您的用例,但是将Global Accelerator链接到ECS链接的ALB可以为我提供静态IP地址,这解决了我的用例。如果您想了解动态内部IP,则需要使用lambda查询ALB。这是很大的努力。以下链接: aws.amazon.com/blogs/networking-and-content-delivery/...
马库斯

AWS ECS更新服务-群集<群集名称>-服务<服务名称> --force-new-deployment为我工作!
gvasquez

3

我创建了一个脚本,用于将更新的Docker映像部署到ECS上的登台服务,以便相应的任务定义引用Docker映像的当前版本。我不确定我是否遵循最佳做法,因此欢迎您提供反馈。

为了使脚本正常工作,您需要一个备用ECS实例或一个deploymentConfiguration.minimumHealthyPercent值,以便ECS可以窃取实例以将更新的任务定义部署到该实例。

我的算法是这样的:

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

我的代码粘贴如下:

部署EC

#!/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)

_common.py

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

@Andris谢谢,已修复。
aknuds1

5
这太过分了。应该可以通过terraform或仅通过ecs-cli线进行部署。
霍尔姆斯

@holms我正在使用Terraform更新ECS任务映像。与上面的python代码一样,这太过激了。所需的步骤非常复杂。
Jari Turkia

3

AWS CodePipeline。

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


2
您可以链接到任何文档吗?
BenDog

1

以下情况对我有用,以防docker image标签相同:

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

1

遇到同样的问题。花了几个小时后,完成了以下简化步骤,用于自动部署更新的映像:

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>:latest。最新密钥负责处理各个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。实施时只需将其替换为您各自的区域即可。


我注意到参数是地形参数;关于如何实现CloudFormation的任何想法:我有AutoScalingGroup MinSize:0和MaxSize:1;还有什么需要设置的?
韦恩

0

如上所述,我使用AWS CLI尝试了aws ecs update-service。没有从ECR获取最新的docker。最后,我重新运行了创建ECS集群的Ansible剧本。运行ecs_taskdefinition时,任务定义的版本会增加。那一切都很好。拾取新的Docker映像。

完全不确定任务版本更改是否强制重新部署,或者使用ecs_service的剧本是否导致任务重新加载。

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


我相信仅当您更新实际任务定义配置时才需要修订任务定义。在这种情况下,如果您使用带有最新标签的图像,则无需修改配置?当然,将commit id作为标记是很好的,并且也具有单独的任务定义修订版,以便您可以回滚,但是CI会看到您用于容器的所有凭据,这不是我想要实现的方式。
霍尔姆斯

0

好吧,我也试图找到一种自动化的方法,那就是将更改推送到ECR,然后由服务选择最新的标签。正确的是,您可以通过从群集中停止服务任务来手动执行此操作。新任务将拉出更新的ECR容器。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.