繁体   English   中英

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

完全披露:本教程以 Bitnami 的容器为特色,我为 Bitnami 工作。 然而,这里表达的想法是我自己的,而不是 Bitnami 的意见。

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

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

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

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

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

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

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

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.Push到图片到ECR

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

4.apply force-部署

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

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

我创建了一个脚本,用于将更新的 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

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

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

以下 api 也有效:

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

AWS 代码管道。

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

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

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

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

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

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

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

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

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

我担心的是,一旦我们在 ECR 中上传了像 latest 这样带有 latest 标签的镜像,那么如何部署最新的镜像呢? 它的工作方式是否与K8S使用最新标签的方式相同? 我有时可以看到图像正在自动部署,但有时它不会与 ECS 一起部署。

以下命令对我有用

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM