Como implantar imagens atualizadas do Docker para tarefas do Amazon ECS?

110

Qual é a abordagem certa para fazer com que minhas tarefas do Amazon ECS atualizem suas imagens Docker, uma vez que essas imagens foram atualizadas no registro correspondente?

aknuds1
fonte
Eu recomendaria executar uma função Lambda automatizada / programada. Dessa forma, está fora da instância. Você já tentou isso? Você também pode usar SWF para
executar
Não preciso automatizar @iSkore. Eu gostaria de escrever um script para ele eventualmente, mas eu mesmo escolho quando executá-lo.
aknuds1
Ahh entendi. Não tinha certeza sobre isso. Você pode fornecer um pouco mais de informações?
iSkore
@iSkore Não sei como descrever melhor do que já fiz. O procedimento é: 1. Envie a nova versão da imagem Docker para o registro. 2. Implante uma nova versão de imagem no ECS. A questão é como implementar o último.
aknuds1
isso também não é fácil ou óbvio com o EKS .. como o F é a tarefa mais comum de usar um cluster, implantar uma nova imagem, tão obscura na documentação?

Respostas:

89

Se sua tarefa estiver sendo executada em um serviço, você pode forçar uma nova implantação. Isso força a definição da tarefa a ser reavaliada e a nova imagem do contêiner a ser obtida.

aws ecs update-service --cluster <cluster name> --service <service name> --force-new-deployment
Dima
fonte
1
Acho que para que isso funcione, você precisa se certificar de que há recursos suficientes nas instâncias do ECS para implantar uma tarefa adicional do mesmo tamanho. Presumo que a AWS tenta essencialmente realizar um hotswap, esperando que uma nova instância de tarefa seja pré-inicializada, antes de encerrar a antiga. Ele apenas continua adicionando entradas de "implantações" com 0 instâncias em execução, se você não fizer isso.
Alex Fedulov
3
@AlexFedulov, sim, acho que você está correto. Para não incorrer em tempo de inatividade ao criar uma nova implantação, você pode 1) Provisionar instâncias suficientes para implantar a nova versão junto com a antiga. Isso pode ser alcançado com o escalonamento automático. 2) Use o tipo de implantação Fargate. Você pode evitar a alocação de recursos extras definindo o parâmetro de "porcentagem mínima de integridade" do serviço como 0 para permitir que o ECS remova seu serviço antigo antes de implantar o novo. No entanto, isso causará algum tempo de inatividade.
Dima
3
Opções desconhecidas: --force-new-deployment
user4674453
1
Opções desconhecidas: --force-new-deployment: upgrade awscli
Kyle Parisi
1
Eu tentei este comando, ele não atualiza o contêiner com a nova imagem, ele ativa outro contêiner com a mesma imagem antiga. Então, acabo tendo dois contêineres em execução, embora em serviço eu tenha especificado a contagem desejada = 1
matemática
61

Sempre que você inicia uma tarefa (por meio das chamadas de API StartTaske RunTaskou que é iniciada automaticamente como parte de um serviço), o Agente ECS executará um docker pulldos que imagevocê especificar em sua definição de tarefa. Se você usar o mesmo nome de imagem (incluindo tag) cada vez que for enviar para o registro, poderá fazer com que a nova imagem seja executada executando uma nova tarefa. Observe que se o Docker não conseguir acessar o registro por qualquer motivo (por exemplo, problemas de rede ou problemas de autenticação), o Agente ECS tentará usar uma imagem em cache; se você deseja evitar que imagens em cache sejam usadas ao atualizar sua imagem, você desejará enviar uma tag diferente para seu registro a cada vez e atualizar sua definição de tarefa de forma correspondente antes de executar a nova tarefa.

Atualizar: esse comportamento agora pode ser ajustado por ECS_IMAGE_PULL_BEHAVIORmeio da variável de ambiente definida no agente ECS. Consulte a documentação para obter detalhes. No momento da escrita, as seguintes configurações são suportadas:

O comportamento usado para personalizar o processo de pull de imagem para suas instâncias de contêiner. O seguinte descreve os comportamentos opcionais:

  • Se defaultfor especificado, a imagem é extraída remotamente. Se a extração da imagem falhar, o contêiner usará a imagem em cache na instância.

  • Se alwaysfor especificado, a imagem é sempre puxada remotamente. Se a extração da imagem falhar, a tarefa falhará. Esta opção garante que a versão mais recente da imagem seja sempre obtida. Todas as imagens em cache são ignoradas e estão sujeitas ao processo de limpeza de imagem automatizado.

  • Se oncefor especificado, a imagem será extraída remotamente apenas se não tiver sido extraída por uma tarefa anterior na mesma instância do contêiner ou se a imagem em cache foi removida pelo processo de limpeza de imagem automatizado. Caso contrário, a imagem em cache na instância será usada. Isso garante que nenhum pull de imagem desnecessário seja tentado.

  • Se prefer-cachedfor especificado, a imagem será extraída remotamente se não houver imagem em cache. Caso contrário, a imagem em cache na instância será usada. A limpeza de imagem automatizada é desativada para o contêiner para garantir que a imagem em cache não seja removida.

Samuel Karp
fonte
4
Você tem certeza? Já vi casos em que imagens antigas do docker são executadas mesmo depois de enviar uma nova imagem para o Dockerhub (usando o mesmo nome de tag). Eu acho que talvez eu deva apenas alterar o nome da tag cada vez que uma nova imagem é construída. No entanto, isso tem sido muito raro em minha experiência, então talvez tenham sido apenas problemas de rede momentâneos. (Estou ciente de que você trabalha no ECS, então você é a melhor pessoa para responder isso, mas não é exatamente isso que eu experimentei. Peço desculpas se isso soar rude, não é minha intenção!)
Ibrahim
1
Sim, o comportamento atual é que ele tentará uma tração todas as vezes. Se o pull falhar (problemas de rede, falta de permissões, etc), ele tentará usar uma imagem em cache. Você pode encontrar mais detalhes nos arquivos de log do agente, que geralmente estão em /var/log/ecs.
Samuel Karp,
26

Registrar uma nova definição de tarefa e atualizar o serviço para usar a nova definição de tarefa é a abordagem recomendada pela AWS. A maneira mais fácil de fazer isso é:

  1. Navegue até as definições de tarefas
  2. Selecione a tarefa correta
  3. Escolha criar nova revisão
  4. Se você já estiver obtendo a versão mais recente da imagem do contêiner com algo como a tag: latest, clique em Criar. Caso contrário, atualize o número da versão da imagem do contêiner e clique em Criar.
  5. Expandir ações
  6. Escolha o serviço de atualização (duas vezes)
  7. Em seguida, espere o serviço ser reiniciado

Este tutorial tem mais detalhes e descreve como as etapas acima se encaixam em um processo de desenvolvimento de produto de ponta a ponta.

Divulgação completa: este tutorial apresenta contêineres da Bitnami e eu trabalho para a Bitnami. No entanto, os pensamentos expressos aqui são meus e não a opinião de Bitnami.

Neal
fonte
3
Isso funciona, mas você pode ter que alterar os valores mínimo / máximo do serviço. Se você tiver apenas uma instância do EC2, deverá definir a porcentagem mínima de integridade para zero, caso contrário, a tarefa nunca será encerrada (tornando o serviço temporariamente offline) para implantar o contêiner atualizado.
Malvineous
3
@Malvineous Bom ponto! Na seção de configuração do ECS do tutorial , descrevo exatamente isso. Aqui está a configuração recomendada dessa seção: Número de tarefas - 1, porcentagem mínima saudável - 0, porcentagem máxima - 200.
Neal
@Neal eu tentei sua abordagem conforme declarado aqui ... ainda sem alegria
Hafiz
@Hafiz Se precisar de ajuda para descobrir isso, você deve descrever o quão longe você foi e que erro encontrou.
Neal
Isso funciona apenas para serviços, não tarefas sem serviços.
zaitsman
9

Existem duas maneiras de fazer isso.

Primeiro, use o AWS CodeDeploy. Você pode configurar as seções de implantação Azul / Verde na definição de serviço ECS. Isso inclui um CodeDeployRoleForECS, outro TargetGroup para switch e um Listener de teste (opcional). O AWS ECS criará o aplicativo CodeDeploy e o grupo de implantação e vinculará esses recursos do CodeDeploy ao seu ECS Cluster / Service e ELB / TargetGroups para você. Em seguida, você pode usar CodeDeploy para iniciar uma implantação, na qual você precisa inserir um AppSpec que especifica o uso de qual tarefa / contêiner para atualizar qual serviço. Aqui é onde você especifica sua nova tarefa / contêiner. Em seguida, você verá que novas instâncias são ativadas no novo TargetGroup e o antigo TargetGroup é desconectado do ELB e, em breve, as instâncias antigas registradas no antigo TargetGroup serão encerradas.

Isso parece muito complicado. Na verdade, uma vez que / se você habilitou o escalonamento automático em seu serviço ECS, uma maneira simples de fazer isso é apenas forçar uma nova implantação usando o console ou CLI, como um cavalheiro aqui apontou:

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

Dessa forma, você ainda pode usar o tipo de implantação "rolling update", e o ECS simplesmente ativará novas instâncias e drenará as antigas sem tempo de inatividade do serviço, se tudo estiver OK. O lado ruim é que você perde o controle fino da implantação e não pode reverter para a versão anterior se houver um erro e isso interromperá o serviço em andamento. Mas esta é uma maneira muito simples de fazer.

BTW, não se esqueça de definir os números adequados para porcentagem mínima saudável e porcentagem máxima, como 100 e 200.

Z.Wei
fonte
Existe uma maneira de fazer isso sem precisar mudar o IP? No meu, quando executei isso, funcionou, mas mudou o IP privado que eu estava executando
Migdotcom
@Migdotcom Eu tive um problema semelhante ao precisar de um proxy NLB. Resumindo, a única maneira de manter um IP de instância EC2 igual é usar endereços IP elásticos ou usar uma abordagem diferente. Não sei seu caso de uso, mas vincular o Global Accelerator ao ALB vinculado ao ECS me forneceu endereços IP estáticos, o que resolveu meu caso de uso. Se você quiser saber IPs internos dinâmicos, precisará consultar o ALB com um lambda. Foi um grande esforço. Link abaixo: aws.amazon.com/blogs/networking-and-content-delivery/…
Marcus
aws ecs update-service --cluster <nome do cluster> --service <nome do serviço> --force-new-deployment funcionou para mim!
gvasquez
3

Criei um script para implantar imagens atualizadas do Docker em um serviço de teste no ECS, de modo que a definição de tarefa correspondente se refira às versões atuais das imagens do Docker. Não sei ao certo se estou seguindo as práticas recomendadas, portanto, comentários serão bem-vindos.

Para que o script funcione, você precisa de uma instância sobressalente do ECS ou de um deploymentConfiguration.minimumHealthyPercentvalor para que o ECS possa roubar uma instância para implantar a definição de tarefa atualizada.

Meu algoritmo é assim:

  1. Marque imagens do Docker correspondentes a contêineres na definição de tarefa com a revisão Git.
  2. Envie as tags de imagem do Docker para os registros correspondentes.
  3. Cancele o registro de antigas definições de tarefa na família de definição de tarefa.
  4. Registre a nova definição de tarefa, agora referindo-se às imagens do Docker marcadas com as revisões atuais do Git.
  5. Atualize o serviço para usar a nova definição de tarefa.

Meu código colado abaixo:

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
aknuds1
fonte
@Andris Obrigado, consertado.
aknuds1 de
5
Isso é um exagero. Deve ser possível implantar via terraform ou apenas uma linha ecs-cli.
1919
@holms Estou usando o Terraform para atualizar a imagem da tarefa ECS. Isso é tão exagerado quanto o código python acima. As etapas necessárias são complicadas.
Jari Turkia
3

AWS CodePipeline.

Você pode definir ECR como uma origem e ECS como um destino para implantar.

Cara
fonte
2
você pode vincular a qualquer documentação para isso?
BenDog
1

O seguinte funcionou para mim, caso a tag da imagem do docker fosse a mesma:

  1. Vá para cluster e serviço.
  2. Selecione o serviço e clique em atualizar.
  3. Defina o número de tarefas como 0 e atualize.
  4. Após a conclusão da implantação, redimensione o número de tarefas para 1.
SaiNageswar S
fonte
1

Encontrou o mesmo problema. Depois de passar horas, concluí estas etapas simplificadas para implantação automatizada de imagem atualizada:

1. Mudanças na definição da tarefa ECS: Para um melhor entendimento, vamos supor que você criou uma definição de tarefa com os detalhes abaixo (nota: esses números mudariam de acordo com a definição da sua tarefa):

launch_type = EC2

desired_count = 1

Em seguida, você precisa fazer as seguintes alterações:

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. Marque sua imagem como < nome-da-sua-imagem>: mais recente . A chave mais recente se encarrega de ser puxada pela respectiva tarefa do 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. Empurre para a imagem para ECR

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

4. Aplicar desdobramento forçado

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

Observação: escrevi todos os comandos presumindo que a região seja us-east-1 . Basta substituí-lo por sua respectiva região durante a implementação.

Abhishek Sinha
fonte
Percebi que os parâmetros são parâmetros do terreno; Alguma idéia de como conseguir o mesmo para CloudFormation: I have my AutoScalingGroup MinSize: 0 and MaxSize: 1; o que mais precisa ser definido?
Wayne
0

Usando o AWS cli, experimentei o aws ecs update-service conforme sugerido acima. Não foi obtido o docker mais recente do ECR. No final, executei novamente meu manual do Ansible que criou o cluster ECS. A versão da definição da tarefa é alterada quando ecs_taskdefinition é executado. Então está tudo bem. A nova imagem da janela de encaixe é selecionada.

Sinceramente, não tenho certeza se a alteração da versão da tarefa força a reimplantação ou se o manual usando o ecs_service faz com que a tarefa seja recarregada.

Se alguém estiver interessado, terei permissão para publicar uma versão limpa de meu manual.

mpechner
fonte
Acredito que a revisão da definição de tarefa é necessária apenas quando você atualiza a configuração de definição de tarefa real. neste caso, se você estiver usando imagem com uma tag mais recente, não há necessidade de modificar a configuração? É claro que ter o ID de commit como uma tag é bom, e ter uma revisão de definição de tarefa separada também para que você possa fazer rollback, mas então seu CI verá todas as credenciais que você está usando para o contêiner, que não é a maneira que eu quero implementar.
19h
0

Bem, também estou tentando encontrar uma maneira automatizada de fazer isso, ou seja, enviar as alterações para ECR e, em seguida, a tag mais recente deve ser selecionada pelo serviço. Certo, você pode fazer isso manualmente, parando a tarefa para seu serviço de seu cluster. Novas tarefas extrairão os contêineres ECR atualizados.

Avijeet
fonte