O blog da AWS
Utilizando runtimes customizados no Amazon ECS
Vinicius Schettino, Engenheiro DevOps, Passei Direto
Rodrigo Martins, Engenheiro DevOps, Passei Direto
Gabriel Bella Martini, Arquiteto de Soluções, AWS Brasil Setor Público
Thiago Pádua, Arquiteto de Soluções, AWS Brasil Setor Público
A PasseiDireto criou uma solução para executores self-hosted do GitHub Actions com o Amazon ECS, fornecendo aos times de engenharia uma plataforma escalável e dinâmica para a execução de centenas de workflows de CI/CD. Contudo, com a crescente adoção da proposta, o modelo Docker-in-a-Docker (DinD) escolhido apresentou algumas limitações durante a execução de dezenas de tarefas em paralelo. Essa limitação era esperada pela abordagem de compartilhar o socket de comunicação Docker entre os containers, com a opção privileged. Para aumentar a segurança e a confiabilidade da solução apresentada, foi disponibilizado o sysbox no cluster Amazon ECS, um runtime Docker alternativo que fornece maior isolamento entre os containers. Através de uma AMI personalizada, as instâncias do cluster podem suportar dezenas de executores autocontidos, cada um com sua instalação Docker independente e prontos para desempenhar todas as funções do GitHub Actions sem interferência externa.
Problemas encontrados
Antes do modelo paralelo, para executar diferentes tipos de testes no código de uma aplicação, era necessário criar um executor e instanciar cada um dos testes dentro de uma mesma tarefa, com etapas sequenciais:
Esse processo tem algumas limitações. Primeiro, o funcionamento sequencial aumenta o tempo de build consideravelmente, especialmente quando existem várias etapas custosas que poderiam ser paralelizadas. Segundo, não é possível usar o construtor matrix do GitHub Actions para lançar várias tarefas parecidas em instâncias diferentes (como por exemplo testes em várias versões do Node ou do Python, como iremos mostrar no final desse artigo). Assim, o objetivo era permitir a execução do seguinte fluxo utilizando os executores self-hosted escaláveis no ECS:
O fluxo acima, apesar de funcionar bem em condições de teste ou com poucas etapas, encontrou problemas na execução em escala. Essa situação começa a emergir com dezenas de usuários ou até mesmo com workflows contendo diversas tarefas em paralelo, como exemplificado na imagem:
Em situações como essa, tarefas começam a falhar aleatoriamente, por falta de disponibilidade do socket de comunicação docker:
Nas próximas seções são detalhados os principais desafios com a abordagem até então utilizada e como é possível utilizar recursos do ECS, AMIs e o EC2 Image Builder para permitir mais escalabilidade e performance nas builds paralelas com o GitHub Actions utilizando executores self-hosted.
Visão geral da solução
Ao criar um cluster ECS, o usuário pode escolher entre dois tipos de recursos computacionais para hospedar tarefas e serviços: instâncias Amazon EC2 ou AWS Fargate, sendo esse último um serviço para containers serverless. As instâncias EC2 contém uma instalação Docker e um ECS Agent, que é um container responsável pela comunicação com o cluster, atribuição de tarefas e controle dos recursos disponíveis. O Docker precisa de um ambiente de runtime, responsável por executar os containers e isolar os recursos necessários de acordo com a especificação da imagem e do usuário. O runtime padrão do Docker é o runC, compatível com o padrão da Open Container Initiative (OCI) e referência para sua definição original. Apesar da sua popularidade e versatilidade, o runC não oferece um isolamento suficiente para execução de containers dentro de containers de forma segura e escalável, abordagem essencial para executores self-hosted do GitHub Actions com todas as funcionalidades utilizando containers. A única abordagem viável requer duas configurações arriscadas: a utilização da configuração privileged durante a execução, que dá ao container poderes administrativos também no hospedeiro e o compartilhamento do caminho /var/lib/docker
em vários containers ao mesmo tempo. Além de insegura, essa abordagem não escala bem quando vários containers são disparados em paralelo:
“This means that if you share your /var/lib/docker
directory between multiple Docker instances, you’re gonna have a bad time. Of course, it might work, especially during early testing. “Look ma, I can docker run ubuntu!” But try to do something more involved (pull the same image from two different instances…) and watch the world burn.” — Jérôme Petazzoni
Com isso, foi necessário buscar uma solução de runtime que permitisse maior isolamento, incluindo o uso de containers dentro de containers sem efeitos colaterais de segurança e impacto na performance/confiabilidade da plataforma. Essa é a proposta do sysbox, que proporciona um ambiente com funcionalidades de isolamento parecidas com máquinas virtuais, com maior separação de recursos e compatibilidade a OCI e, consequentemente, com o runC. Além disso é possível observar maior desempenho na solução quando existe a necessidade de aninhamento de containers, substituindo o mapeamento de volumes padrão do Docker por alternativas mais modernas.
Contudo, o ECS ainda não suporta configurações personalizadas de runtime. Ou seja, não existe uma configuração a nível de Task Definition, Cluster ou Capacity Provider para que o usuário possa escolher diferentes runtimes arbitrariamente de acordo com o perfil das tarefas que precisam ser executadas. A alternativa é criar uma imagem de máquina (AMI) com o sysbox disponível, além das configurações padrão do ECS, como detalhado na imagem:
Apesar de ser possível realizar mudanças nas instâncias EC2 dentro de um cluster ECS a partir das configurações de inicialização, a instalação do sysbox é relativamente demorada e aumentaria o tempo de inicialização dos clusters nos eventos de escalabilidade. Com a abordagem de uma AMI personalizada é possível manter o tempo de inicialização muito próximo da configuração padrão, uma vez que todo o processo de instalação do sysbox e suas dependências ocorre durante a construção da imagem, processo anterior à disponibilização das instâncias. Na próxima seção, são detalhados os passos para a criação da AMI com o AWS Image Builder.
Criando a AMI com Sysbox
A imagem construída através dos passos detalhados aqui está publicamente disponível nesta AMI e é semanalmente atualizada (etapa que também será explicada). A AWS fornece através do serviço Image Builder uma maneira de construir imagens sob demanda ou de forma agendada. A principal entidade desse serviço é a pipeline que pode ser agendada ou manual.
As pipelines contêm 3 outras entidades que merecem destaque:
- Receita (recipe): define as etapas de instalação e configuração do SO e demais software (onde iremos dar maior enfâse a seguir);
- Configuração de infraestrutura: define o tipo de instância EC2, volumes EBS, configurações de rede, grupo de segurança, logs e mais;
- Distribuição: define se o acesso à AMI será público ou privado, a região de distribuição e o padrão de nomenclatura das versões que vão sendo geradas a cada nova execução da pipeline.
Para se configurar a receita da imagem é necessário escolher uma imagem base, seja as fornecidas pela AWS ou através de um AMI ID personalizado. A escolha foi usar uma imagem Linux Ubuntu 20.04 LTS para compatibilidade com todas as funcionalidades do sysbox.
Além da escolha da imagem base, é preciso adicionar a instalação personalizada de software que é separada em componentes reaproveitáveis. É possível utilizar alguns componentes fornecidos pela própria AWS. Na imagem abaixo é possível ver os componentes da nossa receita.
Exceto pela receita, as demais entidades podem ser configuradas direto da criação de pipeline (imagem da esquerda) e posteriormente podem ser gerenciadas de fora da mesma (imagem da direita).
O principal desafio foi criar o componente que instala o sysbox. Isso porque não há um pacote atualizado disponível via yum, apt-get
ou similares, e várias dependências precisam ser manualmente instaladas. A definição do componente é data por um arquivo YAML, conforme a seguir:
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
As etapas InstallDeps
e InstallDocker
são triviais, com a instalação de dependências básicas do sistema e a instalação padrão do Docker. Já no item InstallSysbox
, algumas ações importantes são realizadas:
- Instalação do shiftfs, utilizando as recomendações oficiais, módulo do kernel Linux que possibilita criação de sistemas de arquivo virtuais utilizando user namespaces. Essa é a principal tecnologia que dá suporte ao nível de isolamento que o sysbox oferece. A maior parte dos provedores serviços de nuvem não habilita o shiftfs por padrão nos Ubuntus mais novos.
- Configuração do Docker deamon para utilizar o sysbox como runtime padrão através da configuração no arquivo
/etc/docker/daemon.json
. Esse passo precisa necessariamente ser executado antes da instalação do sysbox, evitando assim que o processo de instalação solicite interação do usuário para decidir se o sysbox deve ser o runtime padrão, bloqueando o processo totalmente automático. - Compilação e instalação do sysbox, utilizando o guia oficial. O pacote
apt-get
do sysbox é relativamente desatualizado e por isso a opção pela compilação.
Na etapa InstallECSAgent são executados as instruções da configuração do ECS manuais, sem maiores modificações. Como todos os passos acima são executados em tempo de compilação da imagem, algumas configurações rápidas são necessárias durante a inicialização das instâncias, para garantir que o agente de controle do cluster ECS seja corretamente executado utilizando o runtime runC e as configurações recomendadas:
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
Os comandos acima podem ser configurados na configuração de execução das instâncias do grupo de autoscaling.
Implantação de exemplo
É possível ilustrar o funcionamento da solução com worfklows do GitHub Actions que envolvem várias etapas em paralelo. Um exemplo comum nos projetos de software é a execução de testes e verificações estáticas em diferentes versões do Node.js. Nesse caso essa etapa é necessária para aprovação de um Pull Request, garatindo que novas modificações não impactem negativamente usuários em nenhuma das versões suportadas do Node.js. Utilizando a action que inicializa um conjunto de executores self-hosted em um cluster ECS, esse fluxo pode ser descrito na sintaxe do GitHub Actions assim:
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 }}
Nesse caso, três tipos de ação (testes unitários, integração e linting) são executados em três versões diferentes do Node.js. Por isso, 9 executores são criados no cluster ECS:
De acordo com os limites do Capacity Provider do cluster, novas instâncias vão sendo criadas para hospedar a nova carga de trabalho. Aos poucos, os novos executores vão sendo absorvidos e começam a processar as tarefas:
Depois de alguns segundos, todos os executores estão processsando as tarefas especificadas na matrix do GitHub:
Com o término das tarefas, o workflow é concluído com sucesso e o Pull Request pode ser aprovado:
Conclusão e próximos passos
O modelo tradicional com workflows sequenciais apresenta vários desafios à Engenharia de Software Contínua e à cultura DevOps, deixando automações mais lentas. A construção de pipelines paralelas diminui o tempo de espera dos desenvolvedores na aprovação dos Pull Requests e no feedback do impacto de suas mudanças do ponto de vista dos usuários. A solução proposta permite reduções significativas no tempo de execução das pipelines, evitando problemas de escala e isolamento do runtime Docker padrão com a introdução do sysbox no cluster ECS através de uma AMI personalizada.
Com a solução atuando durante dois meses, foi possível reunir alguns aspectos passíveis de melhorias nas próximas versões da AMI e da arquitetura como um todo:
- Modelo de atualização automática das instâncias do cluster, garantindo que a versão mais nova da AMI seja sempre utilizada. Essa mudança busca disponibilizar melhorias de performance e segurança automaticamente de acordo com o lançamento semanal de novas versões da AMI.
- Criação de testes para AMIs, garantindo que as novas versões estejam funcionais antes da disponibilização para o público geral.
- Automação da infraestrutura do imagem builder com CDK, utilizando os contrutores equivalentes ao passo a passo apresentando utilizando o console da AWS.
Sobre os autores
Vinícius Schettino é DevOps Engineer na Passei Direto com mais de 10 anos de experiência em engenharia de software. Focado em Data CI/CD, MLOps, qualidade de software e automação.
Rodrigo Martins é DevOps Engineer na Passei Direto com mais de 10 anos de experiência em redes de computadores e arquitetura de software. Focado em melhorias no processo de CI/CD e segurança da informação.
Gabriel Bella Martini é um Arquiteto de Soluções na AWS com foco em clientes de Educação. Tem experiência em diferentes projetos relacionados a Inteligência Artificial e tem grande interesse na área de computação gráfica.
Thiago Pádua é Arquiteto de Soluções na AWS atua com o desenvolvimento e apoio aos parceiros do Setor Público. Trabalhou anteriormente com desenvolvimento de software e integração de sistemas, principalmente na indústria de Telecomunicações. Tem um interesse especial em arquitetura de microserviços, serverless e containers.