Script Terraform para executar uma aplicação Docker na AWS usando ECS Fargate

Neste artigo você vai ver:

No artigo de hoje, vamos apresentar um tutorial para criar uma infraestrutura na AWS utilizando script Terraform com o objetivo de executar aplicações Dockers em cluster no ECS Fargate.  Este mecanismo é interessante, pois a própria AWS gerencia as máquinas virtuais que irão ficar os contêineres.

Já o Terraform é bastante utilizado para criar e alterar a infraestrutura de cloud computing pública de forma segura e eficiente. O script facilita o fluxo de trabalho, pois o código passa a ter controle de versão e assim é possível reproduzir em diversos ambientes de desenvolvimento, homologação e produção.

Se você já tem conhecimento com Docker e Terraform, vale a pena conhecer esta forma de executar aplicações na AWS. Se não, o blog da Zup tem artigos sobre Docker e conteúdos sobre Terraform que te ajudarão a conhecer um pouco mais da tecnologia abordada aqui.

O projeto final criado neste artigo está disponível neste repositório no GitHub

O tutorial

O tutorial será dividido em duas etapas:

  • A primeira etapa é criar uma aplicação Docker usando Node.js e iremos subir a imagem  no ECR (Amazon Elastic Container Registry);
  • A segunda etapa é criar uma infraestrutura na AWS conforme o diagrama e visualizar no navegador a aplicação sendo executada.

Pré-requisitos

Antes de começar é importante que tenha algumas aplicações instaladas no seu computador, neste tutorial estamos utilizando o Docker (versão 20.10.12), AWS CLI (versão 2.9.8), Terraform (versão 1.3.7) e Node.JS

Além disso, na AWS é necessário configurar as permissões no IAM (Identity and Access Management). Ao criar um usuário no IAM, deve-se criar uma chave AWS (access keys) que pode ser inserida no arquivo terraform.tfvars e esta chave serve para conectar o Terraform com a AWS. 

Também temos que definir as permissões de acesso por meio das políticas (Policy), por ser um tutorial de teste foi definida a seguinte lista: AmazonEC2FullAccess, AmazonECS_FullAccess, AmazonVPCFullAccess, EC2InstanceProfileForImageBuilderECRContainerBuilds, IAMFullAccess.

Imagem capa do conteúdo ECS Fargate, onde possui um diagrama AWS mostra um ALB que distribui o tráfego para os targets (aplicações) que estão em duas subnets públicas.

Inclusive, se quiser saber mais sobre IAM, você pode ouvir o nosso podcast. Temos um episódio específico sobre o tema:

Back-end

  1. Primeiro vamos criar uma imagem no Docker, para isso vamos desenvolver uma aplicação simples WEB utilizando Node.JS com Express. Desta forma, crie uma pasta  chamada container_docker e uma inicialização de pacotes npm juntamente com o Express. 
  2. Agora execute npm init –y e depois npm install express. Em seguida, crie um arquivo index.js com o seguinte código abaixo. Caso necessite conferir se está funcionando, utilize o comando node index.js e acesse no navegador utilizando a url http://localhos t:3000.
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => res.send('Hello World!'))
app.listen(port, () => console.log(`listening on port 3000`));

3. Precisamos criar o nosso arquivo chamado Dockerfile, que é o documento utilizado para definir os comandos no contêiner.

FROM node:12.7.0-alpine
 
WORKDIR '/app'
COPY package.json .
RUN yarn
COPY . .
EXPOSE 3000
 
CMD ["node", "index.js"]

Implementação do ECR Terraform

Na segunda etapa deste tutorial, vamos utilizar o serviço AWS ECR, que são repositórios para armazenar e gerir imagens de contêineres. Então, vamos começar a fazer um script Terraform separado da criação da infraestrutura ECS. 

  1. Crie uma pasta chamada ecr para colocarmos os scripts de contêineres;
  2. Agora crie o arquivo ecr.tf com o nome do repositório e a sua política;
  3. Definimos em sua política a retenção das 10 últimas imagens compiladas, assim, as versões mais antigas serão expiradas. O arquivo main.tf e variables.tf estão disponíveis no repositório e contém informações de bibliotecas e acessos da aws, bem como as variáveis que são utilizadas neste projeto.
resource "aws_ecr_repository" "this" {
 name = var.container_image
}


resource "aws_ecr_repository_policy" "this" {
 repository = aws_ecr_repository.this.name
 policy     = <<EOF
 {
   "Version": "2008-10-17",
   "Statement": [
     {
       "Sid": "policy ecr access repository",
       "Effect": "Allow",
       "Principal": "*",
       "Action": [
         "ecr:BatchCheckLayerAvailability",
         "ecr:BatchGetImage",
         "ecr:CompleteLayerUpload",
         "ecr:GetDownloadUrlForLayer",
         "ecr:GetLifecyclePolicy",
         "ecr:InitiateLayerUpload",
         "ecr:PutImage",
         "ecr:UploadLayerPart"
       ]
     }
   ]
 }
 EOF
}




resource "aws_ecr_lifecycle_policy" "this" {
 repository = aws_ecr_repository.this.name


 policy = jsonencode({
   rules = [{
     rulePriority = 1
     description  = "last 10 docker images"
     action = {
       type = "expire"
     }
     selection = {
       tagStatus   = "any"
       countType   = "imageCountMoreThan"
       countNumber = 10
     }
   }]
 })
}

4. Para executar o script Terraform siga os seguintes comandos:

terraform init -backend=true -backend-config="backend.hcl"
terraform plan
terraform apply

Subindo imagem Docker

Ao finalizar o processo de criação do nosso ECR, teremos a infraestrutura do repositório criada no console da AWS da região que estamos usando US East (N. Virginia).

Ao selecionar o checkbox do repositório temos no menu a opção habilitada para selecionar o View Push Commands que abrirá um modal com os  comandos  necessários para subir a nossa imagem do Docker. Note que temos os comandos para Linux/MacOS e também Windows. Abaixo mostramos os comandos em  Linux/MacOS necessários para enviar a imagem Docker para o repositório ECR. Lembre-se de ter Docker e AWS CLI instalado localmente. Após instalar e configurar a AWS CLI iremos autenticar com o Docker. Nos comandos abaixo será necessário alterar para seu ID de conta da AWS  e a região que está utilizando.

aws ecr get-login-password --region region | docker login --username AWS --password-stdin aws_account_id.dkr.ecr.region.amazonaws.com

Depois entre na pasta da aplicação Node e iremos executar o build do projeto.

docker build -t my-app-container .

Depois que a construção da imagem estiver pronta, vamos definir uma tag para a imagem para subir para o repositório.

docker tag my-app-container:latest aws_account_id.dkr.ecr.region.amazonaws.com/my-app-container:latest

Por fim, vamos enviar a imagem para o repositório ECR da AWS.

docker push aws_account_id.dkr.ecr.region.amazonaws.com/my-app-container:latest

Implementação do ECS Fargate Terraform

Agora vamos criar nossa infraestrutura ECS Fargate, uma tecnologia que gerencia os contêineres sem a necessidade de gerenciar as máquinas virtuais, pois a própria AWS se encarrega de alocar os recursos necessários.  

  1. Neste script, vamos criar uma rede VPC para esse projeto, e teremos um load balancer que irá redirecionar as requisições para os contêineres do ECS.  Para facilitar nosso estudo, vamos deixar em uma subnet pública e com IPs públicos alocados para o contêiner ter acesso a internet;
  2. Vamos criar uma nova pasta chamada ecs e colocar os scripts da infraestrutura do ECS. Crie um arquivo locals.tf que será responsável por manter as tags comuns em todo projeto e variáveis da subnet.
locals {


 subnet_ids = { for k, v in aws_subnet.this : v.tags.Name => v.id }


 common_tags = {
   Project   = "ECS Fargate"
   CreatedAt = "2022-12-17"
   ManagedBy = "Terraform ZUP"
   Owner     = "Caio Thomas Oliveira"
   Service   = "ECS Fargate"
 }
}

3. Vamos criar a vpc.tf, que será a parte de rede da AWS, onde iremos colocar nossos contêineres. Então teremos um internet gateway associado a VPC com a nossa subnet pública e as rotas de tabelas (Route Tables) associadas. Vale lembrar que, neste tutorial, vamos utilizar apenas as subnets públicas para facilitar o estudo.

resource "aws_vpc" "this" {
 cidr_block           = "192.168.0.0/16"
 enable_dns_support   = true
 enable_dns_hostnames = true


 tags = merge(local.common_tags, { Name : "Terraform-ECS-Zup VPC" })
}


resource "aws_internet_gateway" "this" {
 vpc_id = aws_vpc.this.id
 tags   = merge(local.common_tags, { Name : "Terraform-ECS-Zup IGW" })
}


resource "aws_subnet" "this" {
 for_each = {
   "pub_a" : ["192.168.1.0/24", "${var.aws_region}a", "Public A"]
   "pub_b" : ["192.168.2.0/24", "${var.aws_region}b", "Public B"]
 }


 vpc_id            = aws_vpc.this.id
 cidr_block        = each.value[0]
 availability_zone = each.value[1]


 tags = merge(local.common_tags, { Name = each.value[2] })
}


resource "aws_route_table" "public" {
 vpc_id = aws_vpc.this.id


 route {
   cidr_block = "0.0.0.0/0"
   gateway_id = aws_internet_gateway.this.id
 }


 tags = merge(local.common_tags, { Name = "Terraform-ECS-Zup Public" })
}


resource "aws_route_table_association" "this" {
 for_each = local.subnet_ids


 subnet_id      = each.value
 route_table_id = aws_route_table.public.id
}


4. Neste momento, vamos criar nosso Load Balancer (ALB), que será responsável por redirecionar as requisições para os contêineres. Nós utilizamos o Security Group com o objetivo de liberar a porta 80 onde a pessoa usuária final irá fazer as requisições.

resource "aws_lb" "this" {
 name               = "Terraform-ECS-Zup-ALB"
 security_groups    = [aws_security_group.alb.id]
 load_balancer_type = "application"


 subnets = [aws_subnet.this["pub_a"].id, aws_subnet.this["pub_b"].id]


 tags = merge(local.common_tags, { Name = "Terraform ECS ALB" })


}


resource "aws_lb_target_group" "this" {
 name        = "ALB-TG"
 port        = 80
 protocol    = "HTTP"
 target_type = "ip"
 vpc_id      = aws_vpc.this.id


 health_check {
   healthy_threshold   = "3"
   interval            = "30"
   protocol            = "HTTP"
   matcher             = "200,301,302"
   path                = "/"
   timeout             = "5"
   unhealthy_threshold = "5"
 }
}


resource "aws_lb_listener" "this" {
 load_balancer_arn = aws_lb.this.arn
 port              = 80
 protocol          = "HTTP"


 default_action {
   type             = "forward"
   target_group_arn = aws_lb_target_group.this.arn
 }
}


resource "aws_security_group" "alb" {
 name        = "Terraform-ECS-Zup-ALB-SG"
 description = "SG-ALB-ZUP"
 vpc_id      = aws_vpc.this.id




 ingress {
   protocol    = "tcp"
   from_port   = 80
   to_port     = 80
   cidr_blocks = ["0.0.0.0/0"]
 }


 egress {
   from_port   = 0
   to_port     = 0
   protocol    = "-1"
   cidr_blocks = ["0.0.0.0/0"]
 }


 tags = merge(local.common_tags, { Name : "Terraform ECS ALB-SG" })
}

5. No arquivo main.tf, vamos criar nosso ECS cluster, definido a task com a imagem do repositório; as portas do contêiner, que no caso é 3000; quantidade de memória; e a CPU;

6. Também definimos o ECS service que é do tipo FARGATE, as subnets que serão públicas e os IPs públicos para acesso de internet dentro do contêiner;

7. Além disso, definimos o Security Group no ALB (load balancer) que possui objetivo de controlar o tráfego de entrada e saída,  bloquear portas e blocos de IPs.

resource "aws_ecs_cluster" "this" {
 name = var.cluster_name
}


resource "aws_ecs_task_definition" "this" {
 family                   = var.cluster_task
 container_definitions    = <<DEFINITION
 [
   {
     "name": "${var.cluster_task}",
     "image": "${var.image_url}",
     "essential": true,
     "portMappings": [
       {
         "containerPort": ${var.container_port},
         "hostPort": ${var.container_port}
       }
     ],
     "memory": ${var.memory},
     "cpu": ${var.cpu}
   }
 ]
 DEFINITION
 requires_compatibilities = ["FARGATE"]
 network_mode             = "awsvpc"
 memory                   = var.memory
 cpu                      = var.cpu
 execution_role_arn       = aws_iam_role.ecsTaskExecutionRole.arn
}


resource "aws_iam_role" "ecsTaskExecutionRole" {
 name               = "ecsTaskExecutionRole2"
 assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
}


data "aws_iam_policy_document" "assume_role_policy" {
 statement {
   actions = ["sts:AssumeRole"]


   principals {
     type        = "Service"
     identifiers = ["ecs-tasks.amazonaws.com"]
   }
 }
}


resource "aws_iam_role_policy_attachment" "ecsTaskExecutionRole_policy" {
 role       = aws_iam_role.ecsTaskExecutionRole.name
 policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}




resource "aws_ecs_service" "this" {
 name                = var.cluster_service
 cluster             = aws_ecs_cluster.this.id
 task_definition     = aws_ecs_task_definition.this.arn
 launch_type         = "FARGATE"
 scheduling_strategy = "REPLICA"
 desired_count       = var.desired_capacity


 load_balancer {
   target_group_arn = aws_lb_target_group.this.arn
   container_name   = aws_ecs_task_definition.this.family
   container_port   = var.container_port
 }


 network_configuration {
   subnets          = [aws_subnet.this["pub_a"].id, aws_subnet.this["pub_b"].id]
   security_groups  = [aws_security_group.this.id]
   assign_public_ip = true
 }


}


resource "aws_security_group" "this" {
 name        = "Terraform-ECS-Zup TASK SG"
 description = "Terraform-ECS-Zup SG"
 vpc_id      = aws_vpc.this.id


 ingress {
   protocol        = "tcp"
   from_port       = var.container_port
   to_port         = var.container_port
   security_groups = [aws_security_group.alb.id]
 }


 egress {
   protocol    = "-1"
   from_port   = 0
   to_port     = 0
   cidr_blocks = ["0.0.0.0/0"]
 }
}

8. No arquivo variables.tf será necessário pegar o link da imagem do Docker no ECR e definir na variável “image_url”. Neste arquivo também temos as chaves AWS necessárias para executar o Terraform e outras definições que poderão ser alteradas de acordo com o projeto, como: CPU, memória e nomes de tarefas, cluster e serviços;

9. Utilizando o comando da AWS CLI via terminal aws ecs list-clusters, temos a resposta do nome do nosso cluster definido como: my-cluster. Para ver as instâncias que estão em execução utilize o comando aws ecs list-tasks –cluster my-cluster;

10. Note que temos como resultado a task executando três instâncias Docker em servidores ECS Fargate diferentes para garantir a disponibilidade. Usando o comando aws ecs describe-services –service my-first-service –cluster my-cluster temos o retorno de informações importantes que também podem ser vistas no console da AWS. São detalhes do processo, no qual temos a VPC, que encontra a aplicação com as subnets públicas que definimos. Também ficou definido o IP público para ter acesso a internet para montar o contêiner;

11. Ao executarmos o comando aws elbv2 describe-load-balancers –name Terraform-ECS-Zup-ALB, conseguimos recuperar informações do load bancers criado, no qual é responsável por receber as requisições na porta 80 e redirecionar para determinado contêiner na porta 3000. É importante lembrar que este serviço possui as definições de configuração do Target com o Health Checks;

12. Tal  tarefa possui a função de requisitar periodicamente o status do servidor e validar se o serviço está ativo e, assim, garantir que existem contêineres funcionando de acordo com o número definido de instâncias desejadas. Por fim, no DSN name da resposta, temos a URL para conseguirmos acessar a aplicação pelo navegador.

Pontos de atenção

  • Sobre as variáveis aws_access_key e aws_secret_key, o recomendável é utilizar por meio da configuração de ambiente para evitar problemas de segurança da cloud. Tenha sempre cuidado ao publicar esse tipo de informações em códigos públicos e privados;
  • Não estamos liberando acessos aos contêineres em seus respectivos Ip públicos apesar de estar em uma subnet pública. Caso deseje, é necessário habilitar no Security Group. 

Conclusão

O artigo mostra uma forma simples de criar uma infraestrutura para executar contêineres Docker na AWS. Uma grande vantagem é a escalabilidade e a disponibilidade de sua aplicação quando se tem grande demanda de acesso. 

Além disso, o uso do ECS Fargate possibilita o gerenciamento de servidores onde serão alocados os recursos e a orquestração de contêiner definidas nas configurações. Ao utilizar o Terraform, possibilitamos a manutenção para futuras mudanças que poderão ser executadas por meio de scripts

Uma outra abordagem do ECS é utilizar uma subnet privada com um NAT ligado a uma subnet pública e este conectado a subnet privada, ou adotar uma outra solução que é utilizar o AWS Private Link para conectar serviços AWS ou terceiros sem a necessidade de expor na internet. 

Por fim, caso deseje destruir toda a infraestrutura do ambiente criada neste tutorial  basta utilizar o comando do terraform destroy.

Esperamos que tenha apreciado o tutorial que preparamos e aguardamos a sua opinião nos comentários do artigo. Por hoje é isso, continue acompanhando a nossa central de conteúdos!

Referências

Imagem capa do conteúdo ECS Fargate, onde uma mulher branca está de frente para o seu computador na sua empresa. A tela do dispositivo mostra códigos e ao fundo, colegas de trabalho.
caio-thomas
Desenvolvedor de Software
Possui graduação em Sistemas de Informação (2014) e Mestrado em Ciências da Computação (2017) pela Universidade Federal de Uberlândia. Atualmente é desenvolvedor de software sênior na Zup, com experiência no desenvolvimento para o mercado financeiro e no setor elétrico (smart grids).

Artigos relacionados

Este site utiliza cookies para proporcionar uma experiência de navegação melhor. Consulte nossa Política de Privacidade.