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

How do I deploy updated Docker images to Amazon ECS tasks?

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

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

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

The behavior used to customize the pull image process for your container instances. The following describes the optional behaviors:

  • If default is specified, the image is pulled remotely. If the image pull fails, then the container uses the cached image on the instance.

  • If always is specified, the image is always pulled remotely. If the image pull fails, then the task fails. This option ensures that the latest version of the image is always pulled. Any cached images are ignored and are subject to the automated image cleanup process.

  • If once is specified, the image is pulled remotely only if it has not been pulled by a previous task on the same container instance or if the cached image was removed by the automated image cleanup process. Otherwise, the cached image on the instance is used. This ensures that no unnecessary image pulls are attempted.

  • If prefer-cached is specified, the image is pulled remotely if there is no cached image. Otherwise, the cached image on the instance is used. Automated image cleanup is disabled for the container to ensure that the cached image is not removed.

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

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

我的算法是这样的:

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

下面粘贴了我的代码:

deploy-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

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

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

This tutorial 有更多详细信息,并描述了上述步骤如何适应 end-to-end 产品开发过程。

完全披露:本教程主要介绍来自 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 获取最新的 docker。最后,我重新运行创建 ECS 集群的 Ansible 剧本。 ecs_taskdefinition 运行时,任务定义的版本会发生变化。然后一切都很好。新的 docker 图像被拾取。

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

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

AWS 代码管道。

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

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

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

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

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

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

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

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

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

  1. 转到集群和​​服务。
  2. Select 服务并点击更新。
  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.Tag 您的图像为 <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.Push 将图像转为 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。实施时将其替换为您各自的区域即可。

因为 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. 用服务中的旧图像停止过时的旧任务

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

另一种方法是始终在您的管道中使用 aws ecs update 命令 来构建您的图像以用于 ECS 任务,并且在您构建图像后立即执行 - 只需执行部队部署。

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