¿Cómo implemento imágenes de Docker actualizadas en tareas de Amazon ECS?

110

¿Cuál es el enfoque correcto para que mis tareas de Amazon ECS actualicen sus imágenes de Docker, una vez que dichas imágenes se hayan actualizado en el registro correspondiente?

aknuds1
fuente
Recomendaría ejecutar una función Lambda automatizada / programada. De esta forma queda fuera de la instancia. ¿Has probado eso? También puede utilizar SWF para realizar pasos a la vez
iSkore
No necesito automatizarlo @iSkore. Me gustaría escribir un script para él eventualmente, pero elijo cuándo ejecutarlo.
aknuds1
Ahh te pillo. No estaba seguro de eso. ¿Puede proporcionarnos un poco más de información?
iSkore
@iSkore No sé cómo describirlo mejor de lo que ya lo hice. El procedimiento es: 1. Inserte la nueva versión de la imagen de Docker en el registro. 2. Implemente una nueva versión de imagen en ECS. La pregunta es cómo implementar este último.
aknuds1
esto tampoco es fácil ni obvio con EKS ... ¿cómo la F es la tarea más común de usar un clúster, implementar una nueva imagen, tan oscura en la documentación?

Respuestas:

88

Si su tarea se ejecuta bajo un servicio, puede forzar una nueva implementación. Esto obliga a reevaluar la definición de la tarea y extraer la nueva imagen del contenedor.

aws ecs update-service --cluster <cluster name> --service <service name> --force-new-deployment
Dima
fuente
1
Creo que para que esto funcione, debe asegurarse de que haya suficientes recursos en sus instancias de ECS para implementar una tarea adicional del mismo tamaño. Supongo que AWS intenta esencialmente realizar un hotswap, esperando que se inicie previamente una nueva instancia de tarea, antes de finalizar la anterior. Simplemente sigue agregando entradas de "implementaciones" con 0 instancias en ejecución, si no lo hace.
Alex Fedulov
3
@AlexFedulov, sí, creo que tienes razón. Para no incurrir en tiempo de inactividad al crear una nueva implementación, puede 1) Aprovisionar suficientes instancias para implementar la nueva versión junto con la versión anterior. Esto se puede lograr con el ajuste de escala automático. 2) Utilice el tipo de implementación de Fargate. Puede evitar la asignación de recursos adicionales estableciendo el parámetro de "porcentaje mínimo en buen estado" del servicio en 0 para permitir que ECS elimine su antiguo servicio antes de implementar el nuevo. Sin embargo, esto generará un tiempo de inactividad.
Dima
3
Opciones desconocidas: --force-new-deployment
user4674453
1
Opciones desconocidas: --force-new-deployment: upgrade awscli
Kyle Parisi
1
Probé este comando, no actualiza el contenedor con una nueva imagen, hace girar otro contenedor con la misma imagen anterior. Así que termino teniendo dos contenedores en ejecución aunque en servicio tengo un recuento específico deseado = 1
matemáticas
61

Cada vez que inicie una tarea (ya sea a través de las llamadas StartTasky RunTaskAPI o que se inicie automáticamente como parte de un Servicio), el Agente de ECS realizará una docker pullde las imageque especifique en la definición de la tarea. Si usa el mismo nombre de imagen (incluida la etiqueta) cada vez que ingresa a su registro, debería poder ejecutar la nueva imagen ejecutando una nueva tarea. Tenga en cuenta que si Docker no puede acceder al registro por algún motivo (por ejemplo, problemas de red o problemas de autenticación), el agente de ECS intentará utilizar una imagen en caché; Si desea evitar que se usen imágenes almacenadas en caché cuando actualice su imagen, querrá insertar una etiqueta diferente en su registro cada vez y actualizar la definición de la tarea correspondientemente antes de ejecutar la nueva tarea.

Actualización: este comportamiento ahora se puede ajustar a través de la ECS_IMAGE_PULL_BEHAVIORvariable de entorno establecida en el agente de ECS. Consulte la documentación para obtener más detalles. En el momento de escribir este artículo, se admiten las siguientes configuraciones:

El comportamiento utilizado para personalizar el proceso de extracción de imágenes para sus instancias de contenedor. A continuación se describen los comportamientos opcionales:

  • Si defaultse especifica, la imagen se extrae de forma remota. Si la extracción de la imagen falla, el contenedor usa la imagen almacenada en caché en la instancia.

  • Si alwaysse especifica, la imagen siempre se extrae de forma remota. Si la extracción de la imagen falla, la tarea falla. Esta opción garantiza que siempre se extraiga la última versión de la imagen. Las imágenes almacenadas en caché se ignoran y están sujetas al proceso de limpieza de imágenes automatizado.

  • Si oncese especifica, la imagen se extrae de forma remota solo si no ha sido extraída por una tarea anterior en la misma instancia de contenedor o si la imagen en caché fue eliminada por el proceso de limpieza de imágenes automatizado. De lo contrario, se utiliza la imagen almacenada en caché en la instancia. Esto asegura que no se intente extraer imágenes innecesarias.

  • Si prefer-cachedse especifica, la imagen se extrae de forma remota si no hay una imagen en caché. De lo contrario, se utiliza la imagen almacenada en caché en la instancia. La limpieza automática de imágenes está deshabilitada para el contenedor para garantizar que la imagen almacenada en caché no se elimine.

Samuel Karp
fuente
4
¿Estás seguro? He visto casos en los que se ejecutan imágenes antiguas de la ventana acoplable incluso después de haber enviado una nueva imagen a Dockerhub (con el mismo nombre de etiqueta). Supongo que tal vez debería tocar el nombre de la etiqueta cada vez que se crea una nueva imagen. Sin embargo, esto ha sido bastante raro en mi experiencia, por lo que tal vez fueron solo problemas de red momentáneos. (Soy consciente de que trabaja en ECS, por lo que es la mejor persona para responder a esto, pero esto no es exactamente lo que he experimentado. ¡Disculpe si esto parece una mala educación, no mi intención!)
Ibrahim
1
Sí, el comportamiento actual es que intentará un tirón cada vez. Si la extracción falla (problemas de red, falta de permisos, etc.), intentará utilizar una imagen en caché. Puede encontrar más detalles en los archivos de registro del agente que suelen estar en formato /var/log/ecs.
Samuel Karp
26

Registrar una nueva definición de tarea y actualizar el servicio para usar la nueva definición de tarea es el enfoque recomendado por AWS. La forma más sencilla de hacerlo es:

  1. Navegar a Definiciones de tareas
  2. Seleccione la tarea correcta
  3. Elija crear nueva revisión
  4. Si ya está extrayendo la última versión de la imagen del contenedor con algo como la etiqueta: latest, simplemente haga clic en Crear. De lo contrario, actualice el número de versión de la imagen del contenedor y luego haga clic en Crear.
  5. Expandir acciones
  6. Elija Servicio de actualización (dos veces)
  7. Luego espere a que se reinicie el servicio

Este tutorial tiene más detalles y describe cómo los pasos anteriores encajan en un proceso de desarrollo de producto de un extremo a otro.

Divulgación completa: este tutorial presenta contenedores de Bitnami y yo trabajo para Bitnami. Sin embargo, los pensamientos expresados ​​aquí son míos y no la opinión de Bitnami.

Neal
fuente
3
Esto funciona, pero es posible que tenga que modificar los valores mínimo / máximo de su servicio. Si solo tiene una instancia EC2, debe establecer el porcentaje mínimo saludable en cero; de lo contrario, nunca terminará la tarea (haciendo que su servicio esté temporalmente fuera de línea) para implementar el contenedor actualizado.
Malvineous
3
@Malvineous ¡Buen punto! En la sección de configuración de ECS del tutorial , describo exactamente eso. Aquí está la configuración recomendada de esa sección: Número de tareas - 1, Porcentaje mínimo en buen estado - 0, Porcentaje máximo - 200.
Neal
@Neal Probé su enfoque como se indica aquí ... todavía no hay alegría
Hafiz
@Hafiz Si ​​necesita ayuda para resolver esto, debe describir qué tan lejos llegó y qué error encontró.
Neal
Esto solo funciona para servicios, no para tareas sin servicios.
zaitsman
8

Hay dos maneras de hacer esto.

Primero, use AWS CodeDeploy. Puede configurar las secciones de implementación azul / verde en la definición del servicio ECS. Esto incluye un CodeDeployRoleForECS, otro TargetGroup para conmutador y un escucha de prueba (opcional). AWS ECS creará el grupo de implementación y la aplicación CodeDeploy y vinculará estos recursos de CodeDeploy con su clúster / servicio ECS y sus grupos ELB / TargetGroups por usted. Luego, puede usar CodeDeploy para iniciar una implementación, en la que debe ingresar una AppSpec que especifica el uso de qué tarea / contenedor para actualizar qué servicio. Aquí es donde especifica su nueva tarea / contenedor. Luego, verá que se activan nuevas instancias en el nuevo TargetGroup y el antiguo TargetGroup se desconecta del ELB, y pronto se cancelarán las antiguas instancias registradas en el antiguo TargetGroup.

Suena muy complicado. En realidad, dado que / si ha habilitado el escalado automático en su servicio ECS, una forma sencilla de hacerlo es simplemente forzar una nueva implementación usando la consola o cli, como un caballero señaló aquí:

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

De esta manera, puede seguir utilizando el tipo de implementación de "actualización continua", y ECS simplemente activará nuevas instancias y agotará las antiguas sin tiempo de inactividad de su servicio si todo está bien. El lado malo es que pierde el control sobre la implementación y no puede volver a la versión anterior si hay un error y esto interrumpirá el servicio en curso. Pero esta es una forma realmente sencilla de hacerlo.

Por cierto, no olvide establecer los números adecuados para el porcentaje mínimo saludable y el porcentaje máximo, como 100 y 200.

Z.Wei
fuente
¿Hay alguna forma de hacer esto sin tener que cambiar la IP? En el mío, cuando ejecuté esto, funcionó pero cambió la IP privada que estaba ejecutando
Migdotcom
@Migdotcom Tuve un problema similar cuando necesitaba un proxy NLB. En resumen, la única forma de mantener la misma IP de una instancia EC2 es utilizar direcciones IP elásticas o utilizar un enfoque diferente. No conozco su caso de uso, pero vincular Global Accelerator al ALB vinculado a ECS me proporcionó direcciones IP estáticas, esto resolvió mi caso de uso. Si desea conocer las IP internas dinámicas, deberá consultar el ALB con una lambda. Fue un gran esfuerzo. Enlace a continuación: aws.amazon.com/blogs/networking-and-content-delivery/…
Marcus
aws ecs update-service --cluster <nombre de clúster> --service <nombre de servicio> --force-new-deployment funcionó para mí!
gvasquez
3

Creé un script para implementar imágenes de Docker actualizadas en un servicio de ensayo en ECS, de modo que la definición de tarea correspondiente se refiera a las versiones actuales de las imágenes de Docker. No sé con certeza si sigo las mejores prácticas, por lo que agradeceríamos recibir comentarios.

Para que el script funcione, necesita una instancia de ECS de repuesto o un deploymentConfiguration.minimumHealthyPercentvalor para que ECS pueda robar una instancia para implementar la definición de tarea actualizada.

Mi algoritmo es así:

  1. Etiquete las imágenes de Docker correspondientes a los contenedores en la definición de la tarea con la revisión de Git.
  2. Inserte las etiquetas de imagen de Docker en los registros correspondientes.
  3. Anule el registro de definiciones de tareas antiguas en la familia de definiciones de tareas.
  4. Registre una nueva definición de tarea, ahora refiriéndose a las imágenes de Docker etiquetadas con las revisiones actuales de Git.
  5. Servicio de actualización para utilizar una nueva definición de tarea.

Mi código pegado a continuación:

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
fuente
@Andris Gracias, arreglado.
aknuds1
5
Esto es una exageración. Debería ser posible implementar a través de terraform o simplemente una sola línea ecs-cli.
holms
@holms Estoy usando Terraform para actualizar la imagen de la tarea ECS. Eso es tan exagerado como el código de Python anterior. Los pasos necesarios son igualmente complicados.
Jari Turkia
3

AWS CodePipeline.

Puede establecer ECR como fuente y ECS como destino para la implementación.

Chico
fuente
2
¿Puede vincular a alguna documentación para esto?
BenDog
1

Lo siguiente funcionó para mí en caso de que la etiqueta de la imagen de la ventana acoplable sea la misma:

  1. Vaya a clúster y servicio.
  2. Seleccione el servicio y haga clic en actualizar.
  3. Establezca el número de tareas en 0 y actualice.
  4. Una vez finalizada la implementación, vuelva a escalar el número de tareas a 1.
SaiNageswar S
fuente
1

Me encontré con el mismo problema. Después de pasar horas, he concluido estos pasos simplificados para la implementación automatizada de la imagen actualizada:

1.Cambios en la definición de la tarea de ECS: para una mejor comprensión, supongamos que ha creado una definición de tarea con los detalles a continuación (nota: estos números cambiarían en consecuencia según la definición de su tarea):

launch_type = EC2

desired_count = 1

Entonces necesitas hacer los siguientes cambios:

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. Etiquete su imagen como < su-nombre-de-imagen>: última . La última clave se encarga de que la tarea ECS correspondiente la tire.

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.Presione la imagen a ECR

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

4.aplicar despliegue forzado

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

Nota: He escrito todos los comandos asumiendo que la región es us-east-1 . Simplemente reemplácelo con su región respectiva durante la implementación.

Abhishek Sinha
fuente
Noté que los parámetros son parámetros de terraform; Alguna idea de cómo lograr lo mismo para CloudFormation: tengo mi AutoScalingGroup MinSize: 0 y MaxSize: 1; ¿Qué más se necesita configurar?
Wayne
0

Usando AWS cli probé aws ecs update-service como se sugirió anteriormente. No recogió la última ventana acoplable de ECR. Al final, volví a ejecutar mi libro de jugadas de Ansible que creó el clúster ECS. La versión de la definición de la tarea cambia cuando se ejecuta ecs_taskdefinition. Entonces todo va bien. Se recoge la nueva imagen de la ventana acoplable.

A decir verdad, no estoy seguro de si el cambio de versión de la tarea obliga a la redistribución o si el libro de jugadas que usa ecs_service hace que la tarea se recargue.

Si alguien está interesado, obtendré permiso para publicar una versión desinfectada de mi libro de jugadas.

mpechner
fuente
Creo que la revisión de la definición de la tarea solo se requiere cuando actualiza la configuración de la definición de la tarea real. en este caso, si está utilizando una imagen con una etiqueta más reciente, ¿no es necesario modificar la configuración? Por supuesto, tener la identificación de confirmación como etiqueta es bueno, y tener una revisión de definición de tarea separada también para que pueda retroceder, pero luego su CI verá todas las credenciales que está usando para el contenedor, que no es la forma en que quiero implementar las cosas.
holms
0

Bueno, también estoy tratando de encontrar una forma automatizada de hacerlo, es decir, impulsar los cambios a ECR y luego el servicio debería recoger la última etiqueta. Puede hacerlo manualmente deteniendo la tarea de su servicio desde su clúster. Las nuevas tareas extraerán los contenedores ECR actualizados.

Avijeet
fuente