BLOG

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

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

Posts relacionados

Newsletter

Inscreva-se para receber nossos conteúdos!