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

时间:2016-01-17 15:36:32

标签: docker docker-registry amazon-ecs

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

12 个答案:

答案 0 :(得分:50)

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

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

  

用于为容器实例自定义拉图像处理的行为。以下描述了可选行为:

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

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

  •   
  • 如果指定了once,则只有在相同容器实例上的上一个任务未提取图像或者自动图像清理过程中删除了缓存图像时,才会远程提取图像。否则,使用实例上的缓存图像。这样可以确保不会尝试不必要的图像拉出。

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

  •   

答案 1 :(得分:40)

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

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

答案 2 :(得分:18)

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

  1. 导航到任务定义
  2. 选择正确的任务
  3. 选择创建新修订
  4. 如果您已经使用类似:latest标签的方式提取最新版本的容器图像,则只需单击“创建”。否则,请更新容器映像的版本号,然后单击“创建”。
  5. 展开操作
  6. 选择更新服务(两次)
  7. 然后等待服务重新启动
  8. This tutorial有更多细节,并描述了上述步骤如何适合端到端的产品开发过程。

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

答案 3 :(得分:4)

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

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。将您的图片标记为您的图片名称>:最新。最新密钥负责 被各自的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 。只需在实施时将其替换为您各自的区域即可。

答案 4 :(得分:3)

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

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

我的算法是这样的:

  1. 使用Git修订版对任务定义中的容器对应的Docker图像。
  2. 将Docker镜像标记推送到相应的注册表。
  3. 取消注册任务定义系列中的旧任务定义。
  4. 注册新任务定义,现在引用标记有当前Git修订版的Docker镜像。
  5. 更新服务以使用新任务定义。
  6. 我的代码粘贴在下面:

    部署-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
    

答案 5 :(得分:3)

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

首先,使用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。

答案 6 :(得分:2)

在docker image标签相同的情况下为我工作:

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

以下api也可以工作:

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

答案 7 :(得分:2)

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

答案 8 :(得分:1)

AWS CodePipeline。

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

答案 9 :(得分:0)

使用AWS cli我尝试了如上所述的aws ecs update-service。没有从ECR拿起最新的码头工人。最后,我重新运行创建ECS群集的Ansible playbook。当ecs_taskdefinition运行时,任务定义的版本会受到影响。一切都很好。拾取新的泊坞窗图像。

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

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

答案 10 :(得分:0)

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

答案 11 :(得分:-1)

以下命令对我有用

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