Minha experiência no AWS ECS com o Copilot


devops aws

Há pouco mais de um mês eu comecei a ficar incomodado com a forma que estávamos fazendo deploy de uma aplicação de um cliente na Amazon. A aplicação rodava numa instância do EC2 configurada manualmente para rodar um ou dois containers. O deploy era feito com o famigerado shell script que conectava na instância, baixava a última versão da imagem e fazia restart dos containers. Não tínhamos um ambiente de staging, pipeline de deployment ou mesmo um gerenciador de secrets funcionando.

Essa situação começou a ficar cada vez mais incômoda a medida que a aplicação evoluía naturalmente. Mais secrets, mais containers e mais serviços em background para gerenciar me fizeram fazer a pergunta: “qual a forma menos burocrática de rodar essa aplicação na AWS?”. As opções que eu tinha eram:

  1. Kubernetes (EKS): Uma excelente ferramenta, mas um tiro de canhão para matar mosca. Além do mais, o time que lida com o backend é pequeno (duas pessoas na época, três agora, eu incluso) e ninguém além de mim tinha experiência com k8s. Por isso procurei uma opção mais “enxuta” que me deixasse apenas executar containers na nuvem;
  2. Elastic Beanstalk: pareceu o ideal para nosso caso de uso na época. No entanto, algo não deu certo quando fui usar o EB e infelizmente meu tempo estava curto para resolver o problema;
  3. Elastic Container Service: O nome do produto já me pareceu um avanço porque é diretamente relacionado com minha pergunta :-) Lendo algumas coisas na Web, descubro que o EB roda sobre o ECS. Com mais alguma busca eu descubro o AWS Copilot CLI, o que me chamou bastante atenção. Como o tempo para buscas estava curto eu logo disse que “vai ser esse mesmo :-)”

A AWS certamente possui outros produtos similares que devem ter passado desapercebidos.

O processo de migração para o ECS via Copilot

O nosso uso do copilot não foi o mais “next, next, next”. Tínhamos uma parte da infrastrutura já configurada com o Terraform/Terragrunt, portanto já tínhamos recursos como VPC (com subnets públicas e privadas), um cluster Aurora RDS, etc. Mesmo assim foi relativamente tranquilo de fazer nosso ambiente de staging:

copilot app init myapp

Esse comando cria uma pasta “copilot” na raiz do seu repositório com um arquivo .workspace contendo algumas informações (apenas o nome da aplicação no meu caso). Você ainda pode passar a flag --domain se tiver o domínio da sua aplicação já configurado no Route53 e se quiser que o copilot aponte um subdomínio (no formato {svcName}.{envName}.{appName}.{domain}, ex: web.staging.myapp.dominio.com.br) para seus serviços expostos via load balancer.

copilot env init \
  --import-vpc-id vpc-000 \
  --import-public-subnets subnet-111,subnet-222 \
  --import-private-subnets subnet-333,subnet-444 \
  --name staging

Esse comando, um pouco mais complicado por causa dos recursos já existentes, inicializa um ambiente (staging no meu caso). O copilot cria (via CloudFormation) a VPC automaticamente se você não as especificar previamente. Além da VPC o Copilot também cria um cluster ECS utilizando o Fargate (serverless), papéis IAM e tudo mais o que é necessário para criar um ambiente isolado.

copilot svc init --name web --svc-type "Load Balanced Web Service" --dockerfile ./Dockerfile

Isso cria a pasta copilot/web com um arquivo manifest.yml e cria alguns outros recursos, como um repositório de imagens do Docker no Elastic Container Registry (ECR). O arquivo manifest.yml vem mais ou menos assim por padrão:

# The manifest for the "web" service.
# Read the full specification for the "Load Balanced Web Service" type at:
#  https://aws.github.io/copilot-cli/docs/manifest/lb-web-service/

# Your service name will be used in naming your resources like log groups, ECS services, etc.
name: web
# The "architecture" of the service you're running.
type: Load Balanced Web Service

image:
  # Docker build arguments.
  # For additional overrides: https://aws.github.io/copilot-cli/docs/manifest/lb-web-service/#image-build
  build: ./Dockerfile
  # Port exposed through your container to route traffic to it.
  port: 80

http:
  # Requests to this path will be forwarded to your service. 
  # To match all requests you can use the "/" path. 
  path: 'web'
  # You can specify a custom health check path. The default is "/".
  # For additional configuration: https://aws.github.io/copilot-cli/docs/manifest/lb-web-service/#http-healthcheck
  # healthcheck: '/'
  # You can enable sticky sessions.
  # stickiness: true

# Number of CPU units for the task.
cpu: 256
# Amount of memory in MiB used by the task.
memory: 512
# Number of tasks that should be running in your service.
count: 1

# Optional fields for more advanced use-cases.
#
#variables:                    # Pass environment variables as key value pairs.
#  LOG_LEVEL: info
#
#secrets:                      # Pass secrets from AWS Systems Manager (SSM) Parameter Store.
#  GITHUB_TOKEN: GITHUB_TOKEN  # The key is the name of the environment variable, the value is the name of the SSM parameter.

# You can override any of the values defined above by environment.
#environments:
#  test:
#    count: 2               # Number of tasks to run for the "test" environment.

Aqui o copilot já mostra uma utilidade imensa, pois ele cria um serviço no ECS, um load balancer com healthcheck e já configura os target groups do load balancer para apontar para o serviço do ECS. Muito provavelmente você vai precisar editar esse arquivo antes de executar o comando de deploy:

copilot svc deploy --name web -e staging

Esse comando faz uma série de coisas:

  1. Faz o build da imagem do docker com a tag apropriada para enviá-la ao ECR;
  2. Autentica e envia a imagem para o ECR;
  3. Atualiza a stack do CloudFormation para esse serviço, finalizando assim o deploy.

Repeti esse processo algumas vezes para adicionar os outros serviços e cronjobs. Algumas outras edições nos manifests me permitiram fazer o deploy para produção dois dias depois sem maiores problemas.

Minhas impressões

O que eu gostei

  1. Infrastructure as Code: na mesma “pegada” do Terraform e Kubernetes, eu posso editar um yml (bem enxuto, diga-se) e rodar um comando de deploy para minha infraestrutura e o ambiente da minha aplicação ficarem do jeito que eu preciso;
  2. Gerenciamento de secrets estupidamente fácil: é só criar o secret no AWS Systems Manager (SSM) parameter store, colocar as tags certinhas, referenciar no manifest (veja a seção secrets no manifest de exemplo acima) e pronto: seu secret vai ficar armazenado de forma segura na Amazon e ficará disponível como uma variável de ambiente para sua aplicação.
  3. Separação de ambientes: é feita de forma bem tranquila dentro dos manifests, sem precisar ficar criando arquivos extras e duplicando configurações que são a mesma para mais de um ambiente;
  4. Add-ons na infrastrutura: e se você precisar de um bucket no S3? O copilot permite que você crie uma pasta addons e coloque arquivos de stack do CloudFormation. Desta forma, você pode criar um arquivo addons/bucket.yml e o copilot aplicará essa stack quando estiver fazendo deploy do seu serviço e injetará qualquer output (ex: o nome do bucket, ou a url do servidor caso você provisione um banco de dados) como variável de ambiente para sua aplicação. Esse caso de adicionar um bucket é tão comum que o próprio copilot já possui um comando próprio para criar o addon;
  5. Possibilidade de aposentar o Terraform: eu gosto bastante do Terraform, mas não faz sentido usá-lo se seu produto está 100% dentro da Amazon. E a forma como é fácil criar/gerenciar stacks do cloudformation como addons dos serviços me faz questionar se o nosso uso atual do terraform é necessário. Um outro ponto relacionado que é bastante positivo é que o código para provisionar a infrastrutura pode ficar todo dentro do repositório da aplicação, o que diminui a fricção para os desenvolvedores contribuírem para a infra;
  6. Documentação: sabe as documentações oficiais da Amazon? Pois é, a do copilot não tem nada a ver com aquilo e por isso eu achei bem melhor. A busca é rápida, os exemplos são diretos, as explicações enxutas e com uma boa cobertura das funcionalidades;
  7. Autoscaling super fácil: apenas especifique qual é o máximo aceitável de CPU e/ou memória que o copilot faz o resto;
  8. Não é necessário ter instâncias EC2: a menos que você tenha um caso de uso bem específico (como uso de GPU ou uma otimização de preços bem específica) o Fargate é uma excelente opção pelo que temos visto. Não sentimos falta do EC2. Infelizmente eu não fiz as contas para saber a diferença de preços entre as duas alternativas.

O que eu achei mais ou menos

  1. Poucos add-ons: Você pode criar qualquer coisa dentro da AWS para a sua aplicação com os add-ons, porém o copilot só tem utilitários para criação de buckets no S3 ou tabelas no DynamoDB (e volmes EFS a partir da versão 1.3.0, que saiu há poucos dias). Eu acharia bem legal se eu pudesse criar um cluster Aurora ou elasticache pelo copilot com apenas um comando;
  2. task run é pesadão no heroku você pode rodar um heroku run bash e um terminal remoto a-la SSH é aberto para você. Na mesma pegada, o kubectl exec permite executar comandos num pod. Com o copilot isso é MUITO MAIS complicado, pois ele cria um repositório no ECR exclusivo para executar o comando. Demora um bom tempo para fazer build e push da imagem e não há a possibilidade de executar algum comando interativamente (como no heroku ou no kubernetes).

O que eu não gostei

  1. Deployments demorados: Ainda não usamos os pipelines do Copilot, portanto os deployments são feitos manualmente pela máquina dos desenvolvedores. O deploy de cada serviço ou job demora entre 2 a 3 minutos para ser concluído. Alguns serviços podem ser implantados em paralelo, mas na minha experiência todo o processo de deployment ainda demora por volta de 5 minutos nos melhores casos;
  2. Visibilidade limitada no deployment/dificuldade de debug: prepare-se para enfrentar a situação em que o seu serviço está com problemas para inicializar (ex: o container pára porque a aplicação não conseguiu conectar com a base de dados). O copilot não te avisará que o deployment está comprometido e simplesmente ficará esperando a stack do cloudformation ser aplicada. Você terá que olhar os logs da aplicação, cancelar a atualização da stack do cloudformation e só poderá tentar outro deploy depois que o rollback for feito.

Conclusão

Se você precisa rodar uma aplicação de média ou alta complexidade na AWS e acha que o Kubernetes pode ser uma alternativa muito pesada, então o ECS com o Copilot pode ser uma excelente alternativa.

Eu não testei o Elastic Beanstalk tanto quanto ele merecia ser testado, no entanto. Espero ter a chance de fazer um teste mais bem feito no futuro. Até lá eu acredito que ele seja uma alternativa viável mas que eu não conheço os problemas.