【问题标题】:How do I deploy updated Docker images to Amazon ECS tasks?如何将更新的 Docker 映像部署到 Amazon ECS 任务?
【发布时间】:2016-04-22 18:18:27
【问题描述】:

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

【问题讨论】:

  • 我建议运行自动/计划的 Lambda 函数。这样它就在实例之外。你试过吗?您还可以使用 SWF 一次执行步骤
  • 我不需要自动化它@iSkore。我最终想为它编写一个脚本,但要自己选择何时运行它。
  • 啊,明白了。不确定。你能提供更多信息吗?
  • @iSkore 我不知道如何描述它比我已经做过的更好。流程为: 1. 将新版本的 Docker 镜像推送到注册表。 2. 将新的镜像版本部署到 ECS。问题是如何实现后者。
  • 这对于 EKS 来说也不是一件容易或明显的事情。F 是如何使用集群、部署新映像的最常见任务,在文档中如此晦涩难懂?

标签: docker docker-registry amazon-ecs


【解决方案1】:

如果您的任务在服务下运行,您可以强制执行新部署。这会强制重新评估任务定义并拉取新的容器映像。

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

【讨论】:

  • 我认为要使此功能正常工作,您需要确保您的 ECS 实例上有足够的资源来部署相同大小的附加任务。我假设 AWS 尝试实质上执行热交换,等待新任务实例预启动,然后终止旧任务实例。如果您不这样做,它只会继续添加具有 0 个正在运行的实例的“部署”条目。
  • @AlexFedulov,是的,我认为你是对的。为了在创建新部署时不会导致停机,您可以 1) 提供足够的实例以将新版本与旧版本一起部署。这可以通过自动缩放来实现。 2) 使用 Fargate 部署类型。您可以通过将服务的“最小健康百分比”参数设置为 0 来避免分配额外资源,以允许 ECS 在部署新服务之前删除您的旧服务。不过,这会导致一些停机时间。
  • 未知选项:--force-new-deployment
  • 未知选项:--force-new-deployment:升级 awscli
  • 我试过这个命令,它不会用新图像更新容器,它会用相同的旧图像启动另一个容器。因此,即使在服务中我指定了所需的计数 =1,我最终也会运行两个容器
【解决方案2】:

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

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

用于为您的容器实例自定义拉取映像过程的行为。以下描述了可选行为:

  • 如果指定了default,则远程拉取镜像。如果镜像拉取失败,则容器使用实例上的缓存镜像。

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

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

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

【讨论】:

  • 你确定吗?即使在我将新图像推送到 Dockerhub(使用相同的标签名称)之后,我也看到过旧 docker 图像运行的实例。我想也许我应该在每次构建新图像时增加标签名称。但是,根据我的经验,这很少见,所以可能只是暂时的网络问题。 (我知道你从事 ECS 工作,所以你是回答这个问题的最佳人选,但这并不是我所经历的。如果这是粗鲁的,我很抱歉,而不是我的意图!)
  • 是的,当前的行为是每次都会尝试拉取。如果拉取失败(网络问题、权限不足等),它将尝试使用缓存的图像。您可以在代理日志文件中找到更多详细信息,这些文件通常位于 /var/log/ecs
  • @SamuelKarp 请看看我的回答
  • 我同意@Ibrahim 的观点,在许多情况下,当使用 Lambda 的 run_task() 调用时,新图像(即使正确加载到 ECR 中)也不会被提取和使用。 CloudWatch 日志显示没有错误;它只是坚持使用旧图像。确实非常令人沮丧!
【解决方案3】:

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

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

This tutorial 有更多详细信息,并描述了上述步骤如何融入端到端产品开发过程。

全面披露:本教程介绍了来自 Bitnami 的容器,我为 Bitnami 工作。然而,这里表达的想法是我自己的想法,而不是 Bitnami 的意见。

【讨论】:

  • 这行得通,但您可能必须更改您的服务最小/最大值。如果您只有一个 EC2 实例,则必须将最小健康百分比设置为零,否则它将永远不会终止任务(使您的服务暂时离线)以部署更新的容器。
  • @Malvineous 好点!在the ECS setup section of the tutorial 中,我准确地描述了这一点。以下是该部分的推荐配置:任务数 - 1,最小健康百分比 - 0,最大百分比 - 200。
  • @Neal 我尝试了这里所说的方法......仍然没有乐趣
  • @Hafiz 如果你需要帮助来解决这个问题,你应该描述你走了多远以及你遇到了什么错误。
  • 这仅适用于服务,不适用于没有服务的任务。
【解决方案4】:

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

首先,使用 AWS CodeDeploy。您可以在 ECS 服务定义中配置蓝/绿部署部分。这包括一个 CodeDeployRoleForECS、另一个用于交换机的 TargetGroup 和一个测试侦听器(可选)。 AWS ECS 将为您创建 CodeDeploy 应用程序和部署组,并将这些 CodeDeploy 资源与您的 ECS 集群/服务和 ELB/TargetGroups 链接起来。然后您可以使用 CodeDeploy 启动部署,其中您需要输入一个 AppSpec 指定使用什么任务/容器来更新什么服务。这是您指定新任务/容器的地方。然后,您会看到新的实例在新的 TargetGroup 中启动,旧的 TargetGroup 与 ELB 断开连接,很快注册到旧 TargetGroup 的旧实例将被终止。

这听起来很复杂。实际上,既然/如果您在 ECS 服务上启用了自动扩展,那么一种简单的方法就是使用控制台或 cli 强制进行新部署,就像这里的一位绅士指出的那样:

aws ecs update-service --cluster &lt;cluster name&gt; --service &lt;service name&gt; --force-new-deployment

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

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

【讨论】:

  • 有没有办法做到这一点而不必改变IP?在我运行它时,它可以工作,但它改变了我正在运行的私有 IP
  • @Migdotcom 我在需要代理 NLB 时遇到了类似的问题。简而言之,保持 EC2 实例 IP 相同的唯一方法是使用弹性 IP 地址或使用不同的方法。我不知道您的用例,但将 Global Accelerator 链接到 ECS 链接的 ALB 为我提供了静态 IP 地址,这解决了我的用例。如果您想知道动态内部 IP,则需要使用 lambda 查询 ALB。这是一个很大的努力。链接如下:aws.amazon.com/blogs/networking-and-content-delivery/…
  • aws ecs update-service --cluster --service --force-new-deployment 为我工作!
【解决方案5】:

遇到了同样的问题。花费数小时后,完成了这些用于自动部署更新映像的简化步骤:

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.apply 强制部署

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

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

【讨论】:

  • 我注意到参数是 terraform 参数;如何为 CloudFormation 实现相同的任何想法:我有我的 AutoScalingGroup MinSize: 0 和 MaxSize: 1;还有什么需要设置的?
【解决方案6】:

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

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

我的算法是这样的:

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

我的代码贴在下面:

部署-ecs

#!/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 谢谢,已修复。
  • 这太过分了。应该可以通过 terraform 或单个 ecs-cli 行进行部署。
  • @holms 我正在使用 Terraform 更新 ECS 任务映像。这和上面的python代码一样矫枉过正。所需的步骤同样复杂。
  • 真的矫枉过正,我在我的答案中放了一个简单的脚本,按照评分最高的答案提出的建议。看看吧。
  • github.com/silinternational/ecs-deploy 看起来有点矫枉过正。 :)
【解决方案7】:

如果 docker 图像标签相同,以下对我有用:

  1. 转到集群和​​服务。
  2. 选择服务并点击更新。
  3. 将任务数设置为 0 并更新。
  4. 部署完成后,将任务数量重新调整为 1。

以下 api 也可以:

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

【讨论】:

    【解决方案8】:

    AWS CodePipeline。

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

    【讨论】:

    • 你能链接到任何文档吗?
    【解决方案9】:

    因为 AWS 方面没有任何进展。我将为您提供一个简单的 python 脚本,该脚本完全执行 DimaSamuel 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. 使用服务中的旧图像停止过时的旧任务

    【讨论】:

    • 做得很好。 Python 使其更具可读性和可修改性。我为自己的部署使用了类似步骤的 bash 脚本。
    【解决方案10】:

    使用 AWS cli,我按照上面的建议尝试了 aws ecs update-service。没有从 ECR 获取最新的 docker。最后,我重新运行了创建 ECS 集群的 Ansible playbook。 ecs_taskdefinition 运行时,任务定义的版本会发生冲突。那么一切都很好。新的 docker 镜像被拾取。

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

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

    【讨论】:

    • 我相信只有在更新实际任务定义配置时才需要修改任务定义。在这种情况下,如果您使用带有标签 latest 的图像,则无需修改配置?当然,将提交 ID 作为标签很好,并且也有单独的任务定义修订版,因此您可以回滚,但是您的 CI 将看到您用于容器的所有凭据,这不是我想要实现的方式。跨度>
    【解决方案11】:

    好吧,我也在尝试找到一种自动化的方法,即将更改推送到 ECR,然后服务应获取最新标签。 是的,您可以通过从集群中停止服务的任务来手动执行此操作。新任务将拉取更新的 ECR 容器。

    【讨论】:

    • 在下面查看我的答案
    【解决方案12】:

    如果您使用任何 IAC 工具来设置您的 ECS 任务(如 terraform),那么您始终可以通过更新任务定义中的图像版本来完成。 Terraform 基本上会替换旧的任务定义并创建新的,ECS 服务将开始使用带有更新图像的新任务定义。

    另一种方法是始终在您的管道中使用 aws ecs update 命令,该命令构建您的映像以用于 ECS 任务,并且在您构建映像后立即执行强制部署。 p>

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

    【讨论】:

      【解决方案13】:

      以下命令对我有用

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

      【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-10-07
      • 2019-09-10
      • 2020-12-30
      • 2016-05-30
      • 1970-01-01
      • 2019-01-25
      • 1970-01-01
      相关资源
      最近更新 更多