Mais um post maneiro da serie de Terraform! Esse assunto tem sido bastante abordado por bastante gente fera aqui no Churrops. Nada melhor do que dar continuidade nesse assunto que eu gosto pra caramba pro meu primeiro post aqui do blog.

Dessa vez vamos tentar replicar uma arquitetura bem bacana, baseada em alguns dos recursos mais utilizados da AWS como Auto Scale Groups e Elastic Load Balancers.

Todos os exemplos apresentados aqui podem ser encontrados nesse repositório no Github.

Estrutura do projeto

Nosso projeto é bem simples, vai contar com dois arquivos de configuração e um arquivo de bootstrap que vai conter as instruções para a configuração do nosso server.

.
├── README.md
├── main.tf
├── user-data
│   └── bootstrap.sh
└── variables.tf

Arquitetura do Projeto

  • Elastic Load Balancer (ELB)
  • Auto Scaling Group com Launch Configuration
  • Instâncias em Multi-AZ
  • Policies para Scale Up e Scale Down da aplicação

Terraform-ELB-ASG

Show me the code!

variables.tf

Vamos começar criando nosso arquivo de configurações que irá armazenar nossas variáveis que vamos replicar para o resto do nosso projeto. Vamos usar bem poucas, sendo elas o key_path, que irá armazenar um caminho local para a chave pública que iremos importar para dentro das instâncias para fazer conexões SSH você deverá alterar aqui, senão sua aplicação vai ficar incessível pra você, a region que irá armazenar somente a região da AWS que iremos realizar o deploy da nossa arquitetura, ami que irá armazenar o id do AMI que vamos usar de base para nosso projetos, nesse caso estaremos utilizando a versão mais recente da distribuição Amazon Linux e finalimente o instance_type que irá armazenar a classe da instância que iremos utilizar de base, nesse caso estaremos utilizando uma das menores possíveis para uso general, uma t2.micro. Modifiquem a vontade 😀

variable "region" {
  description = "AWS Region"
  default = "us-east-1"
}

variable "key_path" {
  description = "Public key path"
  default = "/Users/matheus/.ssh/id_rsa.pub"
}

variable "ami" {
  description = "AMI"
  default = "ami-8c1be5f6" // Amazon Linux
}

variable "instance_type" {
  description = "EC2 instance type"
  default = "t2.micro"
}

main.tf

Nosso arquivo main é um pouco mais longo que o normal. Nele vamos configurar o restante da arquitetura da nossa aplicação. Elé é bem maior que o anterior, e nele vamos utilizar todas as variábeis que criamos anteriormente. A nossa missão aqui é simples

  • Criar um Launch Configuration
  • Vincular nosso Launch Configuration em um Auto Scaling Group
  • Colocar nosso Scaling de instâncias em Multi-AZ
  • Criar um Load Balancer escutando a porta 80
  • Vincular nosso Auto Scaling Group ao Load Balancer
  • Criar uma Policy para Scale UP das máquinas
  • Criar uma Policy para Scale Down das máquinas

Vamos criar 2 security groups, um para as nossas instâncias diretamentamente chamado websg e outro para o nosso ELB chamado elbsg, onde vamos liberar somente a porta 80. Vamos criar uma regra para liberar o acesso SSH também.

Nosso Launch Configuration vai se chamar webcluster, onde vamos utilizar nossa AMI do Amazon Linux, anexar nosso Security Group e apontar nosso user-data, onde ditamos o script inicial de inicilização das nossas máquinas.

No nosso Auto Scaling Group que vamos chamar de scalegroup, vamos fazer referência ao nosso Launch Configuration e definir as availability zones como us-east-1a, us-east-1b e us-east-1c. Desta forma podemos ter alguns níves de alta disponibilidade caso alguma zona específica da Virginia venha a apresentar algum problema.

Vamos definir nossas métricas de Scale Up e Scale Down da seguinte forma:

  • Sempre que a média de processamento do nosso Auto Scaling Group ficar acima de 60% durante 2 checagens com intervalo de 2 minutos entre elas, vamos subir uma nova instância.
  • Sempre que a média de processamento do nosso Auto Scaling Group ficar abaixo de 10% durante 2 checagens com intervalos de 2 minutos entre elas, vamos matar uma instância.
provider "aws" {
    region = "${var.region}" 
}

resource "aws_launch_configuration" "webcluster" {
    image_id=  "${var.ami}"
    instance_type = "${var.instance_type}"
    security_groups = ["${aws_security_group.websg.id}"]
    key_name = "${aws_key_pair.myawskeypair.key_name}"
    user_data = "${file("user-data/bootstrap.sh")}"

    lifecycle {
        create_before_destroy = true
    }
}

resource "aws_key_pair" "myawskeypair" {
    key_name = "myawskeypair"
    public_key = "${file("${var.key_path}")}"
}

resource "aws_autoscaling_group" "scalegroup" {
    launch_configuration = "${aws_launch_configuration.webcluster.name}"
    availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
    min_size = 1
    max_size = 4
    enabled_metrics = ["GroupMinSize", "GroupMaxSize", "GroupDesiredCapacity", "GroupInServiceInstances", "GroupTotalInstances"]
    metrics_granularity="1Minute"
    load_balancers= ["${aws_elb.elb1.id}"]
    health_check_type="ELB"
    tag {
        key = "Name"
        value = "webserver000"
        propagate_at_launch = true
    }
}

resource "aws_autoscaling_policy" "autopolicy" {
    name = "terraform-autoplicy"
    scaling_adjustment = 1
    adjustment_type = "ChangeInCapacity"
    cooldown = 300
    autoscaling_group_name = "${aws_autoscaling_group.scalegroup.name}"
}

resource "aws_cloudwatch_metric_alarm" "cpualarm" {
    alarm_name = "terraform-alarm"
    comparison_operator = "GreaterThanOrEqualToThreshold"
    evaluation_periods = "2"
    metric_name = "CPUUtilization"
    namespace = "AWS/EC2"
    period = "120"
    statistic = "Average"
    threshold = "60"

    dimensions {
        AutoScalingGroupName = "${aws_autoscaling_group.scalegroup.name}"
    }

    alarm_description = "This metric monitor EC2 instance cpu utilization"
    alarm_actions = ["${aws_autoscaling_policy.autopolicy.arn}"]
}

resource "aws_autoscaling_policy" "autopolicy-down" {
    name = "terraform-autoplicy-down"
    scaling_adjustment = -1
    adjustment_type = "ChangeInCapacity"
    cooldown = 300
    autoscaling_group_name = "${aws_autoscaling_group.scalegroup.name}"
}

resource "aws_cloudwatch_metric_alarm" "cpualarm-down" {
    alarm_name = "terraform-alarm-down"
    comparison_operator = "LessThanOrEqualToThreshold"
    evaluation_periods = "2"
    metric_name = "CPUUtilization"
    namespace = "AWS/EC2"
    period = "120"
    statistic = "Average"
    threshold = "10"

    dimensions {
        AutoScalingGroupName = "${aws_autoscaling_group.scalegroup.name}"
    }

    alarm_description = "This metric monitor EC2 instance cpu utilization"
    alarm_actions = ["${aws_autoscaling_policy.autopolicy-down.arn}"]
}

resource "aws_security_group" "websg" {
    name = "security_group_for_web_server"
    ingress {
        from_port = 80
        to_port = 80
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }

    egress {
        from_port = 0
        to_port = 65535
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }

    lifecycle {
        create_before_destroy = true
    }
}

resource "aws_security_group_rule" "ssh" {
    security_group_id = "${aws_security_group.websg.id}"
    type = "ingress"
    from_port = 22
    to_port = 22
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
}

resource "aws_security_group" "elbsg" {
    name = "security_group_for_elb"
    ingress {
        from_port = 80
        to_port = 80
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }

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

    lifecycle {
        create_before_destroy = true
    }
}

resource "aws_elb" "elb1" {
    name = "terraform-elb"
    availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
    security_groups = ["${aws_security_group.elbsg.id}"]

    listener {
        instance_port = 80
        instance_protocol = "http"
        lb_port = 80
        lb_protocol = "http"
    }

    health_check {
        healthy_threshold = 2
        unhealthy_threshold = 2
        timeout = 3
        target = "HTTP:80/"
        interval = 30
    }

    cross_zone_load_balancing = true
    idle_timeout = 400
    connection_draining = true
    connection_draining_timeout = 400

    tags {
        Name = "terraform-elb"
    }
}

resource "aws_lb_cookie_stickiness_policy" "cookie_stickness" {
    name = "cookiestickness"
    load_balancer = "${aws_elb.elb1.id}"
    lb_port = 80
    cookie_expiration_period = 600
}

output "availabilityzones" {
    value = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

output "elb-dns" {
    value = "${aws_elb.elb1.dns_name}"
}

user-data/bootstrap.sh

Dentro da pasta user-data, vamos ter um arquivo chamado bootstrap.sh dentro da pasta user-data, onde vamos utilizar o mesmo como script de inicialização das nossas instâncias. Ele é bem simples. Vamos instalar o apache e modificar o index do servidor pra exibir uma mensagem aleatória. É bem bobo mesmo.

#!/bin/sh
yum install -y httpd
service httpd start
chkconfig httpd on

echo "Hello darkness my old friend" > /var/www/html/index.html

Deploy

Vamos iniciar o provider do Terraform. Para a instalação, você pode consultar o inicio deste ultimo artigo sobre Terraform.

terraform init

init

Agora vamos executar o apply das nossas configurações para fazer o deploy da nossa infraestrutura

terraform apply

apply

apply-complete

Conferindo o Deploy

No painel de EC2, podemos verificar na aba de Instances, Load Balancers, Launch Configurations e Target Groups para conferir se toda a nossa Stack está de pé.

1-ec2

2-elb

Testando a aplicação

Vamos acessar o DNS do nosso Load Balancer para verificar o deploy da nossa instância.

testando-elb

Testando o Auto Scale

Existem várias formas de testar métricas de Auto Scaling. Quando se trata de scaling por overhead de processamento, uma delas é utilizando a ferramenta Stress.

Conectando na nossa instância via SSH, vamos fazer a instalação da mesma via o gerenciador de pacotes.

sudo yum install -y stress

Vamos realizar o stress para forçar os cores da nossa instância. Como nossa classe de instância só possui 1 core, vou utilizar o parametro 1. Adapte aqui conforme o necessário. Além disso vou criar 2 processo de stress de memória da máquina. Vou monitorar a eficiência do teste junto a ferramenta htop.

stress -c 1 --vm 2

stress-test

Vamos aguardar alguns minutos pra ver se nosso Scale UP está funcionando conforme deveria. Após nossos checks de processamento comprovarem que nosso Auto Scaling Group realmente precisa de uma instância a mais, ela automaticamente irá ser iniciada.

1-scale

2-scale.png

Destruindo nosso ambiente

Agora chegou a hora da parte mais legal do Terraform, o comando destroy, onde vamos apagar todas as configurações criadas no apply.

terraform destroy

Espero ter ajudado, pessoal!  Até a próxima!