Deploy de aplicações Docker na AWS usando ECS e Fargate

Publicado originalmente em thecode.pub em 31 de janeiro de 2018.


Nesse post irei demonstrar como você pode fazer o Deploy de sua aplicação Docker na AWS usando ECS e Fargate.

Como exemplo, eu irei fazer o deploy da aplicação do Openjobs. O código fonte pode ser encontrado nesse link.

Eu irei usar o Terraform para criar a infraestrutura, assim consigo controlar todos os recursos criados como código. Se você deseja saber o básico sobre Terraform, leia meu post sobre (inglês).


ECS

O que é ECS?

O Elastic Container Service (ECS) é um serviço da AWS que cuida da orquestração de containers Docker no seu cluster de instâncias EC2. É uma alternativa para o Kubernetes, Docker Swarm, e outros.

ECS Terminology

Para entender o que é o ECS, nós precisamos entender seus termos e definições, que diferem um pouco do mundo Docker.

  • Cluster: É um group de instâncias EC2 que irão hospedar os containers.
  • Task definition: É uma especificação de como o ECS deve rodar sua aplicação. Aqui se define qual imagem usar, mapeamento de portas, configuração de memória, variáveis de ambiente, etc.
  • Service: Services executam e mantêm as tasks rodando dentro do cluster. O Service irá auto recuperar qualquer task que estiver parada, tentando deixar o total delas em execução com o mesmo valor que especificado.

Fargate

Fargate é a tecnologia que nos permite rodar containers no ECS sem a necessidade de gerenciarmos o cluster de servidores EC2. Apenas fazemos o deploy da aplicação Docker e definimos as regras de escalabilidade. Fargate é um método de execução do ECS.

Show me the code

O exemplo completo está no Github.


A estrutura do projeto

Nosso projeto do Terraform é composto pela seguinte estrutura:

├── modules
│ └── code_pipeline 
│ └── ecs 
│ └── networking 
│ └── rds
├── pipeline.tf
├── production.tf
├── production_key.pub
├── terraform.tfvars
└── variables.tf

  • Modules é onde iremos armazenar o código que cuida da criação de um grupo de recurso. Ele pode ser reutilizado por todos os ambientes (Produção, Staging, QA, etc) sem a necessidade de duplicação de código.
  • production.tf é o arquivo que definimos nosso ambiente de produção. Ele executa os módulos passando as variáveis para definir as configurações.
  • pipeline.tf O pipeline pode ser um recurso global sem a necessidade de ser isolado por ambiente. Esse arquivo irá cuidar da criação do pipeline usando o módulo code_pipeline.

A primeira parte, a rede

A branch para essa parte pode ser encontrada aqui.

A primeira coisa que precisamos criar é a VPC com 2 subnets (1 pública e 1 privada) em cada Availability Zone. Cada Availability Zone fica em uma região isolada geograficamente. Mantendo nossos recursos em mais de uma Availability Zone é o primeiro item para termos High Availability. Se um desses locais físicos falham por algum motivo, sua aplicação pode responder dos outros locais.

Nossa rede

Mantendo nosso cluster na subnet privada protege nossa infraestrutura de acesso externo. A subnet privada é configurada para permitir acesso apenas da subnet pública (Apenas o Load Balancer no nosso caso).

Esse é o código para criar essa parte (é praticamente o mesmo do meu post de introdução ao Terraform):

O código acima cria a VPC, 4 subnets (2 públicas e 2 privadas) em cada Availability Zone. Também cria o NAT para permitir a subnet privada acessar a internet.


O Banco de dados

A branch para essa parte pode ser encontrada aqui.

Iremos criar uma instância de banco de dados no RDS. Ele estará localizado na subnet privada, permitindo apenas a public subnet de accessar.

Com esse código criamos o recurso do RDS com os valores recebidos das variáveis. Também criamos o Security Group que deverá ser usado pelos recursos que se comunicam com o banco de dados (no nosso caso, o cluster ECS).

Ok. Agora temos o banco de dados. Vamos finalmente criar nosso cluster ECS para fazer o deploy da aplicação \o/.


O ECS

A branch para essa parte pode ser encontrada aqui.

Estamos nos aproximando dos passos finais. Agora é a parte que definimos os recursos do ECS necessários para nossa aplicação.

O repositório ECR

A primeira coisa que devemos fazer é criar o repositório para armazenar nossas imagens Docker.

O cluster ECS

Agora precisamos de nosso cluster ECS. Mesmo usando Fargate (que não precisa de nenhuma instância EC2), nós devemos definir o cluster ECS para a aplicação.

As Tasks Definitions

Vamos definir 2 Task Definitions

  • Web: Contem a definição da aplicação Web.
  • Db Migrate: Essa tarefa executará apenas o comando para migrar nosso banco de dados e irá morrer. Como é uma tarefa de execução única, não precisamos de um Service para ela.

As Tasks Definitions são configuradas em um arquivo JSON e renderizadas como um template no Terraform.
Essa é a Task Definition para a aplicação Web:

No arquivo acima estamos definindo a Task Definition para o ECS. Nós passamos o repositório ECR criado como uma variável. Também configuramos outras variáveis para que o ECS consiga iniciar nossa aplicação Rails. 
A definição da migração de banco de dados é praticamente a mesma. Nós apenas mudamos o comando que será executado.

O Load Balancer

Antes de criarmos os Serviços, nós precisamos criar o Load Balancer. Ele será colocado na subnet pública e irá enviar as requests para o Service no ECS.

No arquivo acima nós definimos que nosso Target Group irá usar a porta 80 com o protocolo HTTP. Nós também criamos um Security Group para permitir acessos vindos da internet apenas na porta 80. Depois criamos o Application Load Balancer e o Listener. É necessário usar o Application Load Balancer no lugar do Elastic Load Balancer para usar o Fargate.

O ECS Service finalmente

Agora iremos criar o Service. Para usar o Fargate, precisamos especificar o launch_typecom o valorFargate.

O Fargate nos permite escalar nossa aplicação facilmente. Para isso, precisamos apenas criar as métricas no CloudWatch e ativar os gatilhos para escalar para cima e para baixo.

Nós criamos 2 politicas de Auto Scaling. Uma para escalar para cima e outra para escalar para baixo a quantidade desejada de Tasks rodando no nosso Service Web do ECS.

Depois criamos uma métrica no CloudWatch baseada no uso de CPU. Se o uso de CPU está maior que 85% por 2 períodos, a trigger de alarm_actionexecuta a política de Scale Up. Se retornar para o estado Ok, irá executar a política de Scale Down.


O Pipeline para o deploy da aplicação

Nossa infraestrutura para fazer o deploy da aplicação Docker está pronta. Mas ainda é meio tedioso fazer o deploy para o ECS. No nosso caso, precisamos fazer manualmente o envio da imagem para o repositório ECR e atualizar a Task Definition com a nova imagem. Podemos executar isso através do Terraform, mas pode ser melhor se conseguirmos fazer um push no nosso código no Github na master branch e o deploy for feito automaticamente para nós.

Apresentando, CodePipeline e CodeBuild.

CodePipeline é o serviço gerenciado de Continuous Integration e Continuous Delivery da AWS.

CodeBuild é um serviço gerenciado de build que pode executar testes e gerar pacotes para nós (no nosso caso, uma imagem Docker).

Com eles nós podemos criar pipelines para entregar nossa aplicação para o ECS. O fluxo será:

  • Você faz o push do código para a master branch.
  • CodePipeline irá pegar o código no passo de Source e executará o passo de Build (CodeBuild).
  • O passo de Build irá processar nosso Dockerfile, construindo e enviando a imagem para o ECR and chamando o passo de Deploy.
  • O passo de Deploy atualiza nosso cluster ECS com a nova imagem.

Vamos definir nosso Pipeline com o Terraform:

No código acima nós criamos um projeto CodeBuild, usando o seguinte buildspec (arquivo de especificação de build):

Nós definimos algumas fases no arquivo acima..

  • pre_build: Atualiza o aws-cli, configura algumas variáveis de ambiente: REPOSITORY_URL com o repositório ECR e IMAGE_TAG com versão de source do CodeBuild. O repositório ECR é passado como variável pelo Terraform.
  • build: Constrói usando o Dockerfile e gera uma imagem Docker e cria uma tag LATEST no repositório.
  • post_build: Envia a imagem gerada no passo anterior para o repositório. Cria um arquvio chamado imagedefinitions.json com o seguinte conteúdo: 
    ‘[{“name”:”web”,”imageUri”:REPOSITORY_URL”}]’
    Esse arquivo é usado pelo CodePipeline para atualizar nosso cluster ECS no passo de Deploy.
  • artifacts: Pega o arquivo imagedefinitions.jsongerado no passo anterior e usa como o artefato.

Depois é criado o CodePipeline com 3 estagágios:

  • Source: Pega o repositório do Github e executa o próximo passo.
  • Build: Executa o CodeBuild que criamos.
  • Production: Pega o artefato gerado pelo passo de Build (imagedefinitions.json) e faz o deploy para o ECS.

Vamos ver tudo trabalhando junto?


Executando tudo

O código com o exemplo completo pode ser encontrado aqui.

Clone ele. Como usamos o Github como provider de código no CodePipeline, você precisa gerar o token de acesso para o repositório. (https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/)

Depois de gerar o token, exporte como uma variável de ambiente.

$ export GITHUB_TOKEN=YOUR_TOKEN

Agora precisamos importar os módulos e bibliotecas de providers do Terraform.

$ terraform init

Agora deixe a magia começar!

$ terraform apply

Ele irá mostrar que o Terraform irá criar alguns recursos e se você deseja continuar.

Digite yes.

Terraform irá começar a criar nossa infraestrutura.

Sério, pegue um café até ele terminar.

AWESOME!. Nossa infraestrutura está pronta!!. Se você entrar no seu CodePipeline no Dashboard da AWS você ira ver que ele ativou nossa primeira build.

Espere até todos passos ficarem verdes.

Pegue o DNS do seu Load Balancer e verifique a aplicação.

$ terraform output alb_dns_name
It is working \o/

Finalmente, a aplicação está rodando. Magica!

Se tiver alguma dúvida, entre em contato comigo. Esse post foi apenas uma introdução para o ECS usando Fargate e Terraform.

Cheers 🍻