Blog de Amazon Web Services (AWS)

Uso de runtimes personalizados en Amazon ECS

Vinicius Schettino, Ingeniero de DevOps, Directo
Rodrigo Martins, Ingeniero de DevOps, pasado directo
Gabriel Bella Martini, Arquitecto de Soluciones, sector público de AWS Brasil
Thiago Pádua, Arquitecto de Soluciones, sector público de AWS Brasil

 

PasseiDirecto creó una solución para , proporcionando a los equipos de ingeniería una plataforma escalable y dinámica para ejecutar cientos de flujos de trabajo de CI/CD. Sin embargo, con la creciente adopción de la propuesta, el modelo elegido Docker in-a-Docker (DiND), presentó algunas limitaciones durante la ejecución de decenas de tareas en paralelo. Esta limitación se esperaba por el enfoque de compartir el socket de comunicación Docker entre contenedores, con la opción privilegiada. Para aumentar la seguridad y confiabilidad de la solución presentada, sysbox se puso a disposición en el clúster de Amazon ECS, un tiempo de ejecución de Docker alternativo que proporciona un mayor aislamiento entre contenedores. A través de una AMI personalizada, las instancias de clúster pueden admitir docenas de ejecutores autónomos, cada uno con su propia instalación de Docker independiente y listas para realizar todas las funciones de GitHub Actions sin interferencias externas.

 

Problemas encontrados

Antes del modelo paralelo, para ejecutar diferentes tipos de pruebas en el código de una aplicación, era necesario crear un ejecutor e instanciar cada una de las pruebas dentro de la misma tarea, con pasos secuenciales:

 

 

Este proceso tiene algunas limitaciones. En primer lugar, la operación secuencial aumenta considerablemente el tiempo de construcción, especialmente cuando hay varios pasos costosos que podrían paralelizarse. En segundo lugar, no es posible utilizar el constructor de matriz de GitHub Actions para lanzar varias tareas similares en diferentes instancias (como probar en múltiples versiones de Node o Python, como mostraremos al final de este artículo). Por lo tanto, el objetivo era permitir la ejecución del siguiente flujo utilizando los ejecutores autohospedados escalables en ECS:

 

 

El flujo anterior, aunque funciona bien en condiciones de prueba o con pocos pasos, encontró problemas para ejecutarse a escala. Esta situación comienza a surgir con docenas de usuarios o incluso con flujos de trabajo que contienen varias tareas en paralelo, como se ilustra en la imagen:

 

 

En situaciones como esta, las tareas comienzan a fallar aleatoriamente debido a la falta de disponibilidad del socket de comunicación docker:

 

 

En las siguientes secciones se detallan los desafíos clave con el enfoque utilizado hasta ahora y cómo puede utilizar las características de ECS, AMI y EC2 Image Builder para permitir una mayor escalabilidad y rendimiento en compilaciones paralelas con GitHub Actions utilizando ejecutores autoalojados.

Visión General de la solución

Al crear un clúster de ECS, el usuario puede elegir entre dos tipos de recursos de cómputo para alojar tareas y servicios: instancias de Amazon EC2 o instancias de AWS Fargate, estas últimas un servicio para contenedores sin servidor (serverless). Las instancias de EC2 contienen una instalación de Docker y un agente de ECS, que es un contenedor responsable de comunicarse con el clúster, asignar tareas y controlar los recursos disponibles. Docker necesita un entorno de tiempo de ejecución (runtime), responsable de ejecutar contenedores y aislar los recursos requeridos de acuerdo con la imagen y la especificación del usuario. El runtime predeterminado de Docker es RunC, compatible con el estándar Open Container Initiative (OCI) y referencia a su definición original. A pesar de su popularidad y versatilidad, RunC no proporciona suficiente aislamiento para ejecutar contenedores dentro de contenedores de forma segura y escalable, un enfoque esencial para los ejecutores autoalojados de GitHub Actions con toda la funcionalidad utilizando contenedores . El único enfoque viable requiere dos configuraciones arriesgadas: el uso de la configuración privilegiada durante la ejecución, lo que da al contenedor poderes administrativos también en el host y compartir la ruta /var/lib/docker a través de varios contenedores al mismo tiempo. Además de ser inseguro, este enfoque no se escala bien cuando se disparan varios contenedores en paralelo:

« Esto significa que si comparte su directorio/var/lib/docker entre varias instancias de Docker, lo pasará mal.  Por supuesto, podría funcionar, especialmente durante las pruebas tempranas.  « Mira ma, puedo docker run ubuntu!»  Pero trata de hacer algo más complicado (sacar la misma imagen de dos instancias diferentes…) y ver cómo arde el mundo». Jérôme Petazzoni

Por lo tanto, era necesario buscar una solución de runtime que permitiera un mayor aislamiento, incluido el uso de contenedores dentro de contenedores sin efectos secundarios de seguridad e impacto en el rendimiento/confiabilidad de la plataforma. Esta es la propuesta de sysbox, que proporciona un entorno con funcionalidad de aislamiento similar a máquina virtual, con mayor separación de recursos y compatibilidad OCI y consecuentemente con RunC. Además, puede ver un mayor rendimiento en la solución cuando existe la necesidad de anidar contenedores, reemplazando la asignación de volumen estándar de Docker con alternativas más modernas.

Sin embargo, ECS aún no admite configuraciones de runtime personalizadas . Es decir, no hay configuración en el nivel de Definición de Tarea, Clúster o Proveedor de Capacidad para que el usuario pueda elegir diferentes runtimes arbitrariamente de acuerdo con el perfil de las tareas que se deben realizar. La alternativa es crear una imagen de máquina (AMI) con el sysbox disponible, además de la configuración predeterminada de ECS, como se detalla en la imagen:

 

 

Aunque puede realizar cambios en las instancias de EC2 dentro de un clúster de ECS desde la configuración de arranque, la instalación de sysbox requiere relativamente tiempo y aumentaría el tiempo para que los clústeres se inicien en eventos de escalado. Con el enfoque AMI personalizado, puede mantener el tiempo de arranque muy cerca de la configuración predeterminada, ya que todo el proceso de instalación de sysbox y sus dependencias se producen durante la construcción de la imagen, un proceso anterior a la implementación de las instancias. En la siguiente sección, se detallan los pasos para crear la AMI con AWS Image Builder.

Creación de la AMI con Sysbox

La imagen construida a través de los pasos detallados aquí está disponible públicamente en esta AMI y se actualiza semanalmente (paso que también se explicará). AWS proporciona a través del servicio Image Builder una forma de crear imágenes bajo demanda o de forma programada. La entidad principal de este servicio son las tuberías (pipeline), que puede ser programado o manual.

El pipeline contienen otras 3 entidades que vale la pena destacar:

  • Receta: define los pasos para instalar y configurar el sistema operativo y otro software (donde daremos mayor énfasis a continuación);
  • Configuración de infraestructura: define el tipo de instancia EC2, los volúmenes de EBS, la configuración de red, el grupo de seguridad, los logs y más
  • Distribución: Define si el acceso a la AMI será público o privado, la región de distribución y el patrón de nomenclatura de las versiones que se generan con cada nueva ejecución del pipeline.

Para configurar la receta de la imagen, debe elegir una imagen base, ya sea la proporcionada por AWS o mediante un ID de AMI personalizado. La elección fue utilizar una imagen Linux Ubuntu 20.04 LTS para compatibilidad con todas las características de sysbox.

Además de elegir la imagen base, debe agregar una instalación de software personalizada que esté separada en componentes reutilizables. Puede utilizar algunos componentes proporcionados por AWS. En la imagen de abajo puede ver los componentes de nuestra receta.

 

 

Excepto para la receta, otras entidades se pueden configurar directamente desde la creación de pipeline (imagen a la izquierda) y posteriormente se pueden administrar desde fuera del mismo (imagen a la derecha).

 

 

El reto principal era crear el componente que instala sysbox. Esto se debe a que no hay ningún paquete actualizado disponible a través de yum, apt-get o similar, y es necesario instalar varias dependencias manualmente. La definición del componente está fechada por un archivo YAML, de la siguiente manera:

 

name: InstallSysboxECSAgent

description: Install Sysbox and ECS Agent

schemaVersion: 1

phases:

- name: build

steps:

- name: InstallDeps

action: ExecuteBash

inputs:

commands:

- id

- sudo -s

- export TZ=UTC

- apt-get update

- |

apt-get -y install\

apt-transport-https \

ca-certificates \

curl \

gnupg-agent \

wget \

jq \

software-properties-common \

git \

make \

dkms

- name: InstallDocker

action: ExecuteBash

inputs:

commands:

- sudo -s

- >-

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -

- >-

add-apt-repository "deb [arch=amd64]

https://download.docker.com/linux/ubuntu $(lsb_release -cs)

stable"

- apt-get update

- apt-get -y install docker-ce docker-ce-cli containerd.io

- usermod -aG docker ubuntu

- name: InstallSysbox

action: ExecuteBash

inputs:

commands:

- sudo -s

- git clone https://github.com/toby63/shiftfs-dkms.git shiftfs-dkms

- cd shiftfs-dkms && make -f Makefile.dkms

- modprobe shiftfs

- lsmod | grep shiftfs

- >-

echo '{"default-runtime": "sysbox-runc", "runtimes": {

"sysbox-runc": { "path": "/usr/local/sbin/sysbox-runc" } } }' | jq

'.' > /etc/docker/daemon.json

- "git config --system url.https://github.com/.insteadOf git@github.com:"

- git clone --recursive git@github.com:nestybox/sysbox.git

- cd sysbox && make sysbox && make install

- service docker restart

- name: InstallECSAgent

action: ExecuteBash

inputs:

commands:

- sudo -s

- >-

sh -c "echo 'net.ipv4.conf.all.route_localnet = 1' >>

/etc/sysctl.conf"

- sysctl -p /etc/sysctl.conf

- >-

echo iptables-persistent iptables-persistent/autosave_v4 boolean

true | debconf-set-selections

- >-

echo iptables-persistent iptables-persistent/autosave_v6 boolean

true | debconf-set-selections

- apt-get -y install iptables-persistent

- >-

iptables -t nat -A PREROUTING -p tcp -d 169.254.170.2 --dport 80

-j DNAT --to-destination 127.0.0.1:51679

- >-

iptables -t nat -A OUTPUT -d 169.254.170.2 -p tcp -m tcp --dport

80 -j REDIRECT --to-ports 51679

- iptables -A INPUT -i eth0 -p tcp --dport 51678 -j DROP

- sh -c 'iptables-save > /etc/iptables/rules.v4'

- mkdir -p /etc/ecs && touch /etc/ecs/ecs.config

- >-

curl -o ecs-agent.tar

https://s3.us-east-2.amazonaws.com/amazon-ecs-agent-us-east-2/ecs-agent-latest.tar

- docker load --input ./ecs-agent.tar

 

Los pasos InstallDeps e InstallDocker son triviales, con la instalación de dependencias básicas del sistema y la instalación predeterminada de Docker. En el elemento InstallSysBox, se realizan algunas acciones importantes:

  • Instalación de Shiftfs, usando las recomendaciones oficiales, módulo kernel Linux que permite la creación de sistemas de archivos virtuales utilizando espacios de nombres de usuario. Esta es la tecnología principal que soporta el nivel de aislamiento que ofrece sysbox. La mayoría de los proveedores de servicios en la nube no habilitan shiftfs de forma predeterminada en los Ubuntus más reciente.
  • Configuración de Docker deamon para utilizar sysbox como runtime predeterminado mediante la configuración en el archivo /etc/docker/daemon.json. Este paso debe realizarse necesariamente antes de la instalación de sysbox, evitando así que el proceso de instalación solicite la interacción del usuario para decidir si el sysbox debe ser el runtime predeterminado, bloqueando el proceso completamente automático.
  • Compilación e instalación de sysbox utilizando la guía oficial. El paquete sysbox apt-get está relativamente obsoleto y, por lo tanto, la opción de compilación.

En el paso InstallECSAgent, se ejecutan instrucciones manuales de configuración de ECS , sin modificaciones adicionales. Dado que todos los pasos anteriores se realizan en tiempo de compilación de la imagen, se requieren algunas configuraciones rápidas durante el inicio de la instancia para garantizar que el agente de control de clúster de ECS se ejecute correctamente utilizando el runtime de RunC y la configuración recomendada:

 

sudo -s

/usr/local/sbin/sysbox

docker restart

echo ECS_CLUSTER=${cluster.clusterName} | tee /etc/ecs/ecs.config

echo ECS_DATADIR=/data | tee -a /etc/ecs/ecs.config

echo ECS_ENABLE_TASK_IAM_ROLE=true | tee -a /etc/ecs/ecs.config

echo ENABLE_TASK_IAM_ROLE_NETWORK_HOST=true | tee -a /etc/ecs/ecs.config

echo ECS_LOGFILE=/log/ecs-agent.log | tee -a /etc/ecs/ecs.config

echo ECS_AVAILABLE_LOGGING_DRIVERS=[\"json-file\",\"awslogs\"] | tee -a /etc/ecs/ecs.config

echo ECS_LOGLEVEL=info | tee -a /etc/ecs/ecs.config

curl -o ecs-agent.tar https://s3.us-east-2.amazonaws.com/amazon-ecs-agent-us-east-2/ecs-agent-latest.tar

docker load --input ./ecs-agent.tar

docker run --name ecs-agent --privileged --detach=true --restart=on-failure:10 --volume=/var/run:/var/run --volume=/var/log/ecs/:/log:Z --volume=/var/lib/ecs/data:/data:Z --volume=/etc/ecs:/etc/ecs --net=host --userns=host --runtime=runc --env-file=/etc/ecs/ecs.config amazon/amazon-ecs-agent:latest

 

Los comandos anteriores se pueden configurar en la configuración de ejecución de las instancias del grupo de autoescalado.

 

Ejemplo de implementación

Es posible ilustrar cómo funciona la solución con los Worfklows de GitHub Actions que implican varios pasos en paralelo. Un ejemplo común en proyectos de software es ejecutar pruebas estáticas y comprobaciones en diferentes versiones de Node.js. En este caso, este paso es necesario para aprobar una solicitud de extracción, asegurándose de que las nuevas modificaciones no afecten negativamente a los usuarios en ninguna de las versiones compatibles de Node.js. Mediante la acción que inicializa un conjunto de ejecutores autoalojados en un clúster de ECS, este flujo se puede describir en la sintaxis de GitHub Actions de la siguiente manera:

 

jobs:

pre-job:

runs-on: ubuntu-latest

steps:

- uses: aws-actions/configure-aws-credentials@v1

with:

aws-access-key-id: ${{ secrets.AWS_ID }}

aws-secret-access-key: ${{ secrets.AWS_KEY }}

aws-region: ${{ secrets.AWS_REGION }}

- name: Provide a self hosted to execute this job

uses: PasseiDireto/gh-runner-task-action@main

with:

github_pat: ${{ secrets.MY_SECRET_TOKEN }}

task_definition: 'gh-runner'

cluster: 'gh-runner'

task_count: 9 # 3 scripts x 3 versions

test:

needs: pre-job

runs-on: self-hosted

strategy:

matrix:

version: [ 13, 14, 15 ]

script: ["lint", "test-unit -- --coverage=true", "test-integration"]

steps:

- uses: actions/checkout@v2

- uses: actions/setup-node@v2

with:

node-version: ${{ matrix.version }}

- name: Perform checks

run: |

npm ci

npm run ${{ matrix.script }}

 

En este caso, tres tipos de acción (pruebas unitarias, integración y linting) se ejecutan en tres versiones diferentes de Node.js. Por lo tanto, se crean 9 ejecutantes en el clúster de ECS:

 

 

De acuerdo con los límites del proveedor de capacidad del clúster, se están creando nuevas instancias para alojar la nueva carga de trabajo. Poco a poco, los nuevos ejecutantes se absorben y comienzan a procesar las tareas:

 

 

Después de unos segundos, todos los ejecutantes procesan las tareas especificadas en la matriz de GitHub:

 

 

Con la finalización de las tareas, el flujo de trabajo se completa correctamente y se puede aprobar la solicitud de extracción:

 

 

Conclusión y próximos pasos

El modelo tradicional con flujos de trabajo secuenciales presenta varios desafíos para la Ingeniería Continua de Software y la cultura DevOps, lo que ralentiza las automatizaciones. La creación de pipelines paralelos reduce el tiempo de espera de los desarrolladores para aprobar las solicitudes de extracción y la retroalimentación sobre el impacto de sus cambios desde el punto de vista del usuario. La solución propuesta permite reducciones significativas en el tiempo de ejecución de pipelines, evitando problemas de escala y aislamiento del runtime estándar de Docker al introducir sysbox en el clúster de ECS a través de una AMI personalizada.

Con la solución funcionando durante dos meses, fue posible reunir algunos aspectos que podrían mejorarse en las próximas versiones de la AMI y la arquitectura en su conjunto:

  • Actualice automáticamente las instancias de clúster, asegurándose de que siempre se utilice la versión más reciente de AMI. Este cambio busca ofrecer mejoras de rendimiento y seguridad automáticamente a medida que las nuevas versiones de AMI se publican semanalmente.
  • Creación de pruebas para AMI, asegurando que las nuevas versiones funcionen antes de que estén disponibles para el público en general.
  • Automatización de la infraestructura del constructor de imágenes con CDK, utilizando los constructores equivalentes al tutorial que se presenta mediante la consola de AWS.

Este artículo fue traducido del Blog de AWS en Portugués

 


Sobre los autores

Vinícius Schettino es Ingeniero de DevOps en Passi Direto con más de 10 años de experiencia en ingeniería de software. Enfocado en datos CI/CD, MLOP, calidad de software y automatización.

 

 

 

 

Rodrigo Martins es Ingeniero de DevOps en Passi Direto con más de 10 años de experiencia en redes informáticas y arquitectura de software. Centrado en mejoras en el proceso de CI/CD y seguridad de la información.

 

 

 

 

Gabriel Bella Martini es arquitecto de soluciones de AWS con un enfoque en los clientes de educación. Tiene experiencia en diferentes proyectos relacionados con la Inteligencia Artificial y tiene gran interés en computación gráfica.

 

 

 

 

Thiago Pádua es arquitecto de soluciones de AWS trabaja con el desarrollo y el apoyo de socios del sector público. Anteriormente trabajó con desarrollo de software e integración de sistemas, principalmente en la industria de las telecomunicaciones. Tiene un interés especial en microservicios, arquitectura sin servidor y contenedores.