DEV Community

Cover image for Como construir uma aplicação escalável com Terraform e AWS
Ezequiel Lopes
Ezequiel Lopes

Posted on

Como construir uma aplicação escalável com Terraform e AWS

Neste artigo, mostrarei como construir um sistema escalável e resiliente na AWS utilizando Terraform. O sistema será escalável horizontalmente e distribuído em diferentes zonas de disponibilidade. Além disso, terá um health check para verificar a saúde das instâncias.

Resiliência

Um sistema resiliente é capaz de se adaptar a condições anormais e manter seu funcionamento total ou parcial. Para isso, existem várias estratégias tanto para a infraestrutura quanto para o software, como políticas de retry, mensageria, sistema distribuído, health check, redundância, entre outros.

O que acontece se o volume de clientes acessando nosso site for maior do que o planejado? O sistema suportaria esses usuários? O que acontece se a zona de disponibilidade em que nosso sistema está localizado ficar fora do ar? Ou se recebermos um ataque de negação de serviço? Esses eventos não são nossa culpa, mas precisamos ter estratégias para nos proteger quando esse tipo de evento acontecer.

Construiremos uma estratégia que minimiza os riscos, mas não garante resiliência total. Para alcançar resiliência total do seu sistema, prepare-se para criar um servidor no espaço, como mencionado no livro "Designing Data-Intensive Applications".

Terraform

Terraform é uma ferramenta para provisionamento de infraestrutura como código. Facilitando muito todo o processo de criação, edição e deleção de diversos recursos, incluindo redes virtuais, máquinas e sistemas de armazenamento, entre outros. Não se limitando apenas à AWS, com Terraform é possível integrar-se a diversos provedores de nuvem, como Azure, GCP, Oracle e outros, além de ser possível importar projetos existentes neles.

Auto Scaling

Auto Scaling é a capacidade de aumentar ou reduzir o número de workloads de acordo com as necessidades a qualquer momento. A AWS oferece um serviço de Auto Scaling que podemos integrar ao nosso sistema. Permitindo criar mais máquinas com base em métricas como utilização de CPU, memória ou número de requisições. Quando a demanda diminui, as máquinas podem ser removidas, garantindo que os recursos sejam utilizados de forma eficiente e econômica.

Load Balancer

Com várias máquinas trabalhando em conjunto, o Load Balancer tem o papel de decidir para onde cada requisição deve ser direcionada. Existem várias estratégias de balanceamento, desde as mais simples, onde cada requisição é distribuída igualmente entre as máquinas, até as mais complexas, que consideram a carga atual de cada instância.

Bora lá

Nosso sistema funcionará da seguinte maneira:

  • Um load balancer na frente que distribuirá as requisições,
  • Dois security groups, um para o load balancer e outro para as instâncias EC2,
  • Auto Scaling que será responsável por criar as máquinas,
  • Um template que terá as configurações das instâncias.

Primeiramente, vamos executar o comando para iniciar nosso projeto Terraform.

terraform init 
Enter fullscreen mode Exit fullscreen mode

Vamos criar o arquivo de variáveis onde definiremos valores como o nome da aplicação, a região que utilizaremos, o tipo da instância, os valores de saúde, entre outros.

// variables.tf variable "aws_region" { type = string description = "AWS region where the resources will be deployed." default = "us-east-2" } variable "environment" { type = string description = "Name of the deployment environment (e.g., dev, stage, prod)." default = "dev" } variable "service_name" { type = string description = "Name of the service/application to be deployed." default = "app-go" } variable "instance_config" { description = "Configuration for the EC2 instances." type = object({ ami = string type = string key_name = optional(string, null) }) default = { ami = "ami-09040d770ffe2224f" type = "t2.medium" } } variable "alb_health_check_config" { description = "Configuration for the Application Load Balancer (ALB) health checks." nullable = true default = {} type = object({ enabled = optional(bool, true) healthy_threshold = optional(number, 3) interval = optional(number, 30) matcher = optional(string, "200") path = optional(string, "/healthz") port = optional(number, 80) protocol = optional(string, "HTTP") timeout = optional(number, 5) unhealthy_threshold = optional(number, 3) }) } variable "autoscaling_group_config" { description = "Configuration for the Auto Scaling group." default = {} type = object({ desired_capacity = optional(number, 2) min_size = optional(number, 1) max_size = optional(number, 5) health_check_grace_period = optional(number, 320) // Grace period for health checks (seconds). health_check_type = optional(string, "ELB") force_delete = optional(bool, false) }) } variable "autoscaling_policy_cpu" { description = "Configuration for the CPU utilization auto scaling policy." nullable = true default = {} type = object({ enabled = optional(bool, true) name = optional(string, "CPU utilization") disable_scale_in = optional(bool, false) target_value = optional(number, 40) }) } variable "autoscaling_policy_alb" { description = "Configuration for the ALB request rate auto scaling policy." nullable = true default = {} type = object({ enabled = optional(bool, true) name = optional(string, "Load balancer request per minute") disable_scale_in = optional(bool, false) target_value = optional(number, 40) }) } 
Enter fullscreen mode Exit fullscreen mode

No security group, iremos configurar da seguinte forma: no EC2, iremos permitir apenas tráfego vindo do load balancer na porta 80, e no load balancer também iremos permitir apenas a porta 80 vinda da internet.

// security_groups.tf resource "aws_security_group" "alb" { name = "${local.namespaced_service_name}-alb" description = "Allow HTTP internet traffic" vpc_id = aws_vpc.this.id ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = local.internet_cidr_block } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = local.internet_cidr_block } tags = { "Name" = "${local.namespaced_service_name}-alb" } } resource "aws_security_group" "autoscaling_group" { name = "${local.namespaced_service_name}-autoscaling-group" description = "Allow HTTP traffic only through the load balancer" vpc_id = aws_vpc.this.id ingress { from_port = 80 to_port = 80 protocol = "tcp" security_groups = [aws_security_group.alb.id] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = local.internet_cidr_block description = "Allow all traffic" } tags = { "Name" = "${local.namespaced_service_name}-autoscaling-group" } } 
Enter fullscreen mode Exit fullscreen mode

Aqui está a configuração do template que utilizaremos nas instâncias. Temos também um script app-setup.sh de inicialização, que será responsável por configurar e iniciar nosso projeto.

// template.tf resource "aws_launch_template" "this" { name_prefix = local.namespaced_service_name image_id = var.instance_config.ami instance_type = var.instance_config.type user_data = filebase64("app-setup.sh") monitoring { enabled = true } network_interfaces { associate_public_ip_address = true security_groups = [aws_security_group.autoscaling_group.id] } tag_specifications { resource_type = "instance" tags = { "Name" = "${local.namespaced_service_name}-server" } } } 
Enter fullscreen mode Exit fullscreen mode

O app-setup.sh contém as configurações necessárias para iniciar a aplicação de exemplo em Golang. Ele instala o Golang, clona o repositório do projeto de exemplo (https://github.com/infezek/app-alb-terraform.git) e inicia a aplicação.

O servidor de exemplo possui duas rotas: a rota / retorna uma mensagem de "ok" e a zona de disponibilidade em que a instância está rodando, enquanto a rota /healthz retorna uma mensagem de "ok" nos primeiros 60 segundos e depois retorna um erro, fazendo com que o health check a identifique e reinicie a instância.

#!/bin/bash sudo apt update -y sudo apt upgrade -y cd /var/tmp/ wget https://dl.google.com/go/go1.22.4.linux-amd64.tar.gz sudo rm -rf /usr/local/go sudo tar -C /usr/local -xzf go1.22.4.linux-amd64.tar.gz git clone https://github.com/infezek/app-alb-terraform.git alb-app cd /var/tmp/alb-app GOCACHE=/tmp/gocache GOOS=linux GOARCH=amd64 /usr/local/go/bin/go build -o /home/ubuntu/serve -buildvcs=false sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8080 sudo cp alb-http.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable alb-http sudo systemctl restart alb-http 
Enter fullscreen mode Exit fullscreen mode

Agora, definiremos o autoscaling, especificando os valores mínimos e máximos das instâncias, juntamente com as políticas de escalonamento. Utilizaremos duas métricas principais: requisições por minuto e a média de uso da CPU das instâncias. Além disso, configuraremos a VPC para o autoscaling.

// autoscaling.tf resource "aws_autoscaling_group" "this" { name = local.namespaced_service_name desired_capacity = var.autoscaling_group_config.desired_capacity min_size = var.autoscaling_group_config.min_size max_size = var.autoscaling_group_config.max_size health_check_grace_period = var.autoscaling_group_config.health_check_grace_period health_check_type = var.autoscaling_group_config.health_check_type force_delete = var.autoscaling_group_config.force_delete target_group_arns = [aws_alb_target_group.http.id] vpc_zone_identifier = local.public_subnet_ids launch_template { id = aws_launch_template.this.id version = aws_launch_template.this.latest_version } } resource "aws_autoscaling_policy" "rpm_policy" { name = var.autoscaling_policy_alb.name enabled = var.autoscaling_policy_alb.enabled autoscaling_group_name = aws_autoscaling_group.this.name policy_type = "TargetTrackingScaling" target_tracking_configuration { disable_scale_in = var.autoscaling_policy_alb.disable_scale_in target_value = var.autoscaling_policy_alb.target_value predefined_metric_specification { predefined_metric_type = "ALBRequestCountPerTarget" resource_label = "${aws_alb.this.arn_suffix}/${aws_alb_target_group.http.arn_suffix}" } } } resource "aws_autoscaling_policy" "cpu_policy" { name = var.autoscaling_policy_cpu.name enabled = var.autoscaling_policy_cpu.enabled autoscaling_group_name = aws_autoscaling_group.this.name policy_type = "TargetTrackingScaling" target_tracking_configuration { disable_scale_in = var.autoscaling_policy_cpu.disable_scale_in target_value = var.autoscaling_policy_cpu.target_value predefined_metric_specification { predefined_metric_type = "ASGAverageCPUUtilization" } } } 
Enter fullscreen mode Exit fullscreen mode

No nosso load balancer, definiremos as propriedades de redirecionamento, porta, health check, políticas de erro e outros parâmetros. Neste exemplo, utilizaremos a política de redirecionamento round robin, que distribuirá igualmente as requisições entre as instâncias.

// load_balance.tf resource "aws_alb" "this" { name = local.namespaced_service_name internal = false load_balancer_type = "application" security_groups = [aws_security_group.alb.id] subnets = local.public_subnet_ids tags = { "Name" = local.namespaced_service_name } } resource "aws_alb_target_group" "http" { name = local.namespaced_service_name port = 80 protocol = "HTTP" vpc_id = aws_vpc.this.id health_check { enabled = var.alb_health_check_config.enabled port = var.alb_health_check_config.port timeout = var.alb_health_check_config.timeout protocol = var.alb_health_check_config.protocol unhealthy_threshold = var.alb_health_check_config.unhealthy_threshold interval = var.alb_health_check_config.interval matcher = var.alb_health_check_config.matcher path = var.alb_health_check_config.path healthy_threshold = var.alb_health_check_config.healthy_threshold } } resource "aws_alb_listener" "http" { load_balancer_arn = aws_alb.this.arn port = 80 protocol = "HTTP" default_action { type = "forward" target_group_arn = aws_alb_target_group.http.arn } } 
Enter fullscreen mode Exit fullscreen mode

Por fim, podemos executar o comando plan para verificar quais serão as mudanças e, em seguida, utilizar o comando apply para aplicá-las.

terraform plan terraform apply 
Enter fullscreen mode Exit fullscreen mode

Caso queira desfazer tudo que foi feito, basta executar o comando destroy.

terraform destroy 
Enter fullscreen mode Exit fullscreen mode

Considerações finais:

Criar um sistema escalável e resiliente é um trabalho difícil e requer planejamento, uma estratégia sólida e compreensão do que pode acontecer e como lidar com esses cenários.

Distribuir suas instâncias em diferentes regiões garante uma maior resiliência, pois as zonas de disponibilidade possuem requisitos como distância mínima e máxima entre elas e sistemas de rede elétrica e internet diferentes. Isso faz com que uma falha em uma zona não afete outra, tornando muito mais difícil a ocorrência de indisponibilidade em todas as zonas da mesma região.

Outra vantagem desse sistema é que a AWS oferece uma variedade de serviços e maneiras de configuração, como security groups, AZs, regiões, VPCs, EC2, entre outros. Mesmo para uma aplicação pequena, a configuração manual pelo painel pode se tornar repetitiva, um pouco chata e complexa. Ter um sistema de infraestrutura como código (IaC) automatizado facilita o dia a dia e minimiza erros humanos. Além disso, IaC é um passo importante para uma infraestrutura moderna e eficiente.

LinkedIn
Repositório

Top comments (0)