168

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

4

13 回答 13

150

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

aws ecs update-service --cluster <cluster name> --service <service name> --force-new-deployment
于 2018-02-01T21:48:43.890 回答
78

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

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

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

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

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

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

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

于 2016-01-17T22:59:42.300 回答
40

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

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

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

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

于 2017-01-26T02:02:48.400 回答
12

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

首先,使用 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 <cluster name> --service <service name> --force-new-deployment

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

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

于 2019-10-07T19:18:57.443 回答
8

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

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的所有命令。只需在实施时将其替换为您所在的区域即可。

于 2020-06-11T16:48:23.790 回答
6

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

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

以下 api 也适用:

aws ecs update-service --cluster <cluster_name> --service <service_name> --force-new-deployment
于 2020-04-24T05:31:29.583 回答
6

我创建了一个脚本,用于将更新的 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
于 2016-01-20T10:25:47.963 回答
3

AWS 代码管道。

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

于 2019-03-08T16:17:14.507 回答
2

since there has not been any progress at AWS side. I will give you the simple python script that exactly performs the steps described in the high rated answers of Dima and Samuel Karp.

First push your image into your AWS registry ECR then run the script:

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)

This code does:

  1. create a new task with the new image in the service
  2. stop the obsolete old task with the old image in the service
于 2020-09-10T08:10:48.107 回答
0

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

于 2019-05-14T13:19:00.057 回答
0

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

其他方法是始终在您的管道中使用aws ecs update 命令来构建要在 ECS 任务中使用的映像,并且在您构建映像后立即执行强制部署。

aws ecs update-service --cluster clusterName --service serviceName --force-new-deployment
于 2022-01-20T10:34:34.517 回答
0

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

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

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

于 2018-05-23T02:06:51.440 回答
-3

以下命令对我有用

docker build -t <repo> . 
docker push <repo>
ecs-cli compose stop
ecs-cli compose start
于 2018-02-19T13:31:01.853 回答