簡體   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