Fala pessoal, tudo beleza?

Continuando nosso projeto (que discuti aqui), discutiremos neste artigo a criação de um projeto básico para nosso exemplo e começaremos a criar nossos módulos para usarmos em nossos projetos.

Antes de começar, vamos descrever o que criaremos neste capítulo, a ideia original é criar uma arquitetura básica composta por um load balancer e seu respectivos Ec2, para que pudéssemos instalar nossa aplicação e usar o load balancer para realizar o balanceamento de carga. Outro ponto que vale a pena mencionar em nosso projeto é que criamos um VPC para nosso aplicativo, portanto, não usamos mais o VPC padrão fornecido pelo AWS.

Logs

Requisitos básicos

Antes de começar, vamos fazer um checklist do que vamos precisar para essa parte 2 do projeto.

  1. Vamos precisar adicionar mais permissões na nossa policy, você consegue ver a v2 aqui. (esse json contem as permissões que vamos precisar para esse post).
  2. Criação de uma VPC para nosso projeto, que você pode seguir o exemplos nesse link. Para a criação dessa VPC, recomendo que você execute o terraform em uma pasta separada, vou mostrar a criação a seguir.
  3. Para a criação da nova VPC, vamos usar também um usuário novo e atribuir uma policy(AmazonVPCFullAccess) para esse novo usuário.

Primeiros passos

Dando inicio ao nosso desenvolvimento, vamos na primeira parte do post criar um Load Balancing, com uma resposta fixa(fixed-response), só para verificar se está tudo normal. Para fazer isso, abra o arquivo main.tf e adicione o seguinte trecho de código:

main.tf

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
  key_name      = "homer"
  tags = {
    Name = "teste-homer"
  }
}

data "aws_vpc" "default" {
  default = true
}

data "aws_subnet_ids" "default" {
  vpc_id = data.aws_vpc.default.id
}

resource "aws_lb" "this" {
  name               = "teste-lb"
  internal           = true
  load_balancer_type = "application"
  subnets            = data.aws_subnet_ids.default.ids

}

resource "aws_lb_listener" "this" {

  load_balancer_arn = aws_lb.this.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type = "fixed-response"

    fixed_response {
      content_type = "application/json"
      message_body = "{'home': 'teste'}"
      status_code  = "200"
    }
  }
}

Com a ajuda do data sources, obtemos o ID da VPC e subntes default para criar nossos resources. Outro ponto importante que vale ressaltar, é que criamos um Load Balancing interno, assim ele não tem acesso da internet. Como o LB está sendo criado com todos os recursos default, vamos usar também o security group default da sua VPC.

Vale validar se o sg-default tem as regras de inbound/outbound.

Após salvar, basta executar o plan/apply:

$ terraform init
$ terraform plan
$ terraform apply

Ao finalizar a criação dos resources, vamos conectar via ssh na na ec2 e fazer um curl para o DNS do nosso LB que foi criado(esse é um recurso interno).

curl http://internal-teste-lb-1235919022.us-east-1.elb.amazonaws.com
{'home': 'teste'}


Obs 1

Você pode vir a ter 2 problemas para concluir essa etapa, primeiro é o que já foi comentado, em que você vai precisar verificar tem regras de inbound/outbound do seu security group, o segundo problema refere as rotas da sua VPC, para ambos os problemas esse link pode ajudar.

Obs 2

A segunda observação diz respeito a assuntos mais técnicos:

O primeiro assunto é sobre como manipular o tfstate. Como falado no ultimo post vamos usar o recurso de backend fornecido pelo terraform para que possamos deixar nosso tfstate salvo remotamente, isso facilita, pois pode existir ‘N’ pessoas usando esse projeto e dessa maneira, todaa alteração vai ficar sendo salva remotamente no nosso bucket. Nossa configuração do backend vai ficar assim:

terraform {
  backend "s3" {
    bucket = "you-bucket"
    key    = "homer/terraform.tfstate"
    region = "us-east-1"
  }
}

Basicamente toda vez que você executar um plan/apply no terraform ele vai fazer uma consulta no nosso state que está salvo remotamente, assim garantimos que todos da equipe podem executar nosso projeto.

O segundo ponto trata dos recuros default da AWS, pois até esse momento estamos usando a vpc default que a AWS nos oferece e não acho uma boa ideia editar recursos default da AWS, por isso, antes de dar continuidade vamos criar nossa vpc, com suas devidas configurações, para isso.

$ mkdir homer-vpc
$ cd homer-vpc
$ vi main.tf

e vamos adicionar esse trecho do código:

provider "aws" {
  region = local.region
  profile = "homer-vpc"
  shared_credentials_file = "$HOME/.aws/credentials"
}

locals {
  region = "us-east-1"
}

terraform {
  backend "s3" {
    bucket = "<nome-do-bucket>"
    key    = "<nome-do-bucket>/terraform.tfstate"
    region = "us-east-1"
  }
}

################################################################################
# VPC Module
################################################################################

module "vpc_homer" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "3.2.0"

  name = "homer-dev"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c", "us-east-1d"]
  private_subnets = ["10.0.16.0/20", "10.0.32.0/20", "10.0.48.0/20", "10.0.64.0/20"]
  public_subnets  = ["10.0.0.0/23", "10.0.2.0/23", "10.0.4.0/23", "10.0.6.0/23"]

  enable_ipv6 = true

  enable_nat_gateway = true
  single_nat_gateway = true
  one_nat_gateway_per_az = false


  public_subnet_tags = {
    Name = "homer-public-dev"
    environment = "dev"
  }

  private_subnet_tags = {
    Name = "homer-private-dev"
    environment = "dev"
  }

  tags = {
    environment = "dev"
  }

  vpc_tags = {
    Name = "homer-dev"
    environment = "dev"
  }
}

Após isso, basta executar o plan/apply, e vamos criar nossa VPC(vale lembrar que você precisa ter as permissões para isso). Nesse projeto não vou detalhar as recursos da VPC e suas configurações (até fiz isso, mas vi que ficou muito grande), mas a VPC foi criada usando o modulo de VPC da AWS. Você consegue ver mais informações do modulo aqui.

Obs 3

Não existe almoço grátis, então lembre-se de destruir sua infra quando não estiver usando.

$ terraform destroy

O lado bom da vida é que você tem toda sua infra em código, então não tenha medo destruir e recriar quantas vezes forem necessárias.

Relembrar é viver

Vamos recapitular, até esse ponto adicionamos um novo Load Balancing e alguns data sources no nosso arquivo main.tf.

o código até esse momento está aqui.

Nesse momento, nosso código não contem tanta informação ainda, apenas algumas linhas de código, porem quanto mais recursos adicionarmos, mas complexo e difícil de gerenciar ela vai ficar e é por isso que vamos fazer uma pequena alteração no nosso projeto. Vamos criar os seguintes arquivos.

  • data.tf = usaremos para adicionar todos os data sources do projeto.
  • terraform.tf = fica as informações de provider.
  • variables.tf = arquivo contendo algumas variáveis que serão usadas no projeto.
  • main.tf = arquivo central onde contem nossos recurso.

Como falado anteriormente você pode adicionar todo o código em um arquivo e executar ele, entretanto se seus projetos tiverem muito código, ele fica mais complexo e difícil de estruturar. A própia hashicorp faz algumas recomendações de boas praticas que na criação de seus projetos, você pode ver mais cliando aqui.

caso queira ver o código é esse.

Hello World

Como não poderia ser diferente, vamos iniciar nosso projeto com um “Hello World”, que para nosso projeto será a criação de um LB + Ec2 e instalar um Nginx no nosso servidor e fazer algumas requests via curl, para validar que o Load Balancing está fazendo o balanceamento correto.

Nossa estrutura do projeto para essa parte fica assim:

|-- scripts
    |-- userdata.sh
|-- data.tf
|-- main.tf
|-- terraform.tf
|-- variables.tf

data.tf

data "aws_vpc" "selected" {
  tags = {
    Name        = "homer-dev"
    environment = "dev"
  }
}

data "aws_subnet_ids" "public" {
  vpc_id = data.aws_vpc.selected.id

  tags = {
    Name        = "homer-public-dev"
    environment = "dev"
  }
}


data "aws_subnet_ids" "private" {
  vpc_id = data.aws_vpc.selected.id

  tags = {
    Name        = "homer-private-dev"
    environment = "dev"
  }
}

Caso você tenha seguido a mesma nomenclatura que eu basta salvar o arquivo, caso contrario você vai precisar editar e botar suas os valores dos filtros.

terraform.tf

provider "aws" {
  region                  = "us-east-1"
  shared_credentials_file = "$HOME/.aws/credentials"
}

terraform {
  backend "s3" {
    bucket = "homer-s3-state"
    key    = "homer/terraform.tfstate"
    region = "us-east-1"
  }
}

O arquivo main.tf vou dividir em 3 partes para explicar, a primeira parte é sobre a criação do Load Balancing:

resource "aws_lb" "this" {
  name               = var.app_name
  internal           = var.lb_internal
  load_balancer_type = var.lb_type
  subnets            = data.aws_subnet_ids.public.ids
  security_groups    = [aws_security_group.lb.id]

}

resource "aws_lb_listener" "this" {

  load_balancer_arn = aws_lb.this.arn
  port              = var.http_port
  protocol          = var.http_protocol

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

resource "aws_lb_target_group" "this" {

  name = format("%s-tg-dev", var.app_name)

  vpc_id   = data.aws_vpc.selected.id
  port     = var.http_port
  protocol = var.http_protocol
  health_check {
    healthy_threshold   = 3
    unhealthy_threshold = 10
    timeout             = 5
    interval            = 10
    path                = "/"
    port                = var.http_port
  }

  depends_on = [aws_lb.this]

}

Esse trecho de código, cria nosso Load Balancing e atacha um listner, nesse listner atribuímos uma ação default, que basicamente repassa toda request que chegar nele, para o aws_lb_target_group.this. No resource aws_lb_target_group passamos algumas informações de health check, porta e protocolo que serão usado.

resource "aws_instance" "this" {
  count                  = 2
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = var.ec2_type
  key_name               = var.key_name
  user_data              = file("scripts/userdata.sh")
  subnet_id              = tolist(data.aws_subnet_ids.private.ids)[count.index]
  vpc_security_group_ids = [aws_security_group.ec2.id]
  tags = {
    Name = var.app_name
  }
}

resource "aws_lb_target_group_attachment" "this" {
  count            = 2
  target_group_arn = aws_lb_target_group.this.arn
  target_id        = element(aws_instance.this.*.id, count.index)
  port             = var.http_port
}

Nesse trecho, eu estou vinculando a Ec2 que estou criando ao target group que criei, nela também estou usando o count para informar que quero criar 2 ec2 com o mesma configuração.

Na ultima parte, crio algumas regras de sg:

resource "aws_security_group" "ec2" {
  name        = format("%s-ec2-dev", var.app_name)
  description = "homer-ec2"
  vpc_id      = data.aws_vpc.selected.id


  tags = {
    Name = format("%s-ec2-dev", var.app_name)
  }
}

resource "aws_security_group" "lb" {
  name        = format("%s-lb-dev", var.app_name)
  description = "Allow TLS inbound traffic"
  vpc_id      = data.aws_vpc.selected.id

  tags = {
    Name = format("%s-lb-dev", var.app_name)
  }
}

resource "aws_security_group_rule" "ec2_inboud" {
  type                     = "ingress"
  description              = "HTTP from LB"
  from_port                = var.http_port
  to_port                  = var.http_port
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.lb.id
  security_group_id        = aws_security_group.ec2.id
}

resource "aws_security_group_rule" "lb_inboud" {
  type              = "ingress"
  description       = "Allow tcp inbound traffic"
  from_port         = var.http_port
  to_port           = var.http_port
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.lb.id
}

resource "aws_security_group_rule" "lb_out" {
  type                     = "egress"
  description              = "Traffic between LB and Ec2"
  from_port                = var.http_port
  to_port                  = var.http_port
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.ec2.id
  security_group_id        = aws_security_group.lb.id
}

resource "aws_security_group_rule" "ec2_out" {
  type              = "egress"
  description       = "Allow tcp outbound traffic"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.ec2.id
}

Nesse trecho, estamos criando 2 security-group e atribuindo regras para eles.

userdata.sh

#!/bin/bash

sudo apt update -y
sudo apt install nginx -y
curl http://169.254.169.254/latest/meta-data/local-ipv4 -o /var/www/html/index.html

variables.tf

variable "ec2_type" {
  description = "Tipo da instancia que vamos criar."
  type        = string
  default     = "t3.micro"
}

variable "lb_type" {
  description = "Tipo de LB."
  type        = string
  default     = "application"
}

variable "lb_internal" {
  description = "True  ou false"
  type        = bool
  default     = false
}

variable "key_name" {
  description = "Nome da pem que sera usado para conectar na ec2."
  type        = string
  default     = "homer"
}

variable "lb_name" {
  description = "Nome do LB"
  type        = string
  default     = "teste-lb"
}

variable "http_port" {
  description = "Porta em qual o LB vai 'escutar'."
  type        = number
  default     = 80
}

variable "http_protocol" {
  description = "Protocolo usado para se conectar no LB."
  type        = string
  default     = "HTTP"
}

variable "app_name" {
  description = "Nome do projeto"
  type        = string
  default     = "homer"
}

Após editar e salvar, basta executar nosso plan/apply novamente e vamos poder executar novamente nosso curl.

$ while true; do  curl http:/your-dns.com; echo '';done

Bonus round \o/

Caso não queira logar na AWS para pegar o DNS do nosso Load Balancing, basta criar um arquivo de output:

$ vi output.tf

output "lb_dns_name" {
  description = "DNS do load balancer."
  value       = concat(aws_lb.this.*.dns_name, [""])[0]
}

Após salvar, basta executar o plan/apply e terá no output algo como:

...
Apply complete! Resources: 13 added, 0 changed, 0 destroyed.

Outputs:

lb_dns_name = "homer-262491055.us-east-1.elb.amazonaws.com"

Nesse momento, temos nossa infra criada, e rodando nossos servidores com Nginx.

Caso queira ver o código que estamos executando nesse momento, ele encontra-se aqui.

Problema

Nosso projeto está tomando forma nesse momento e já conseguimos criar nosso primeiro webserver, que caso queira pode ser instalado qualquer coisa através do userdata. Agora imagine se precisarmos criar mais 10 aplicações, vamos ter que duplicar todo esse código, certo?? A resposta é sim e não, pois tudo depende a abordagem que você vai usar. O mais comum nesse caso, é criarmos um modulo do nosso projeto, assim conseguimos versionar ele também, e é o que vamos fazer.

O que são módulos???

Módulos são uma forma de você agrupar todos os resources que você precisa para um determinado propósito e reutilizar de maneira mais organizada e padronizada. Um exemplo de como utilizar módulos foi criação da VPC, nele estou passando o código do modulo e no input atribuo os valores que preciso. Mais informações de módulo, você pode ver aqui.

Reutilizando código

Agora que já sabemos o que é um módulo e para que ele serve, vamos começar a refatorar nosso projeto, e deixar ele de uma forma que possamos reutilizar de maneira fácil. A estrutura do nosso modulo vai ficar algo como:

|-- examples
    |-- script
        |-- userdata.sh
    |-- main.tf
|-- data.tf
|-- main.tf
|-- outputs.tf
|-- variables.tf
|-- versions.tf

Nossa estrutura é praticamente igual a anterior, só adicionamos 2 arquivos novos, que são outputs.tf, que iram conter os valores de saída que vamos querer usar e versions.tf que tem informações sobre versão dos nossos providers.

Outro ponto importante é que devemos tentar deixar nosso modulo o mais dinâmico possível, com isso ele poderá ser usado por outros projetos. Pensando nisso, vamos fazer algumas alterações:

Primeiro, vamos criar algumas variáveis a mais.

variables.tf

...
variable "number_ec2" {
  description = "Number of ec2."
  type        = number
  default     = 2
}


variable "user_data" {
  description = "The user data to provide when launching the instance."
  type        = string
  default     = null
}


variable "http_protocol" {
  description = "Protocol for connections from clients to the load balancer."
  type        = string
  default     = "HTTP"

  validation {
    condition     = contains(["HTTP", "HTTPS"], var.http_protocol)
    error_message = "The values are HTTP and HTTPS."
  }
}

variable "health_check" {
  description = "A list of maps containing key/value pair."
  type        = any
  default = {
    interval            = 30
    path                = "/"
    port                = 80
    healthy_threshold   = 3
    unhealthy_threshold = 5
    timeout             = 2
    protocol            = "HTTP"
  }
}

variable "vpc_name" {
  description = "VPC name."
  type        = string
  default     = "homer-dev"
}

variable "environment" {
  description = "Environment name."
  type        = string
  default     = "dev"
}

variable "subnet_public_name" {
  description = "Subnet public name."
  type        = string
  default     = "homer-public-dev"
}

variable "subnet_private_name" {
  description = "Subnet private name."
  type        = string
  default     = "homer-private-dev"
}
...

Depois vamos adicionar essas variáveis no data.tf e no main.tf.

data.tf


...

data "aws_vpc" "selected" {
  tags = {
    Name        = var.vpc_name
    environment = var.environment
  }
}


data "aws_subnet_ids" "public" {
  vpc_id = data.aws_vpc.selected.id

  tags = {
    Name        = var.subnet_public_name
    environment = var.environment
  }
}

data "aws_subnet_ids" "private" {
  vpc_id = data.aws_vpc.selected.id

  tags = {
    Name        = var.subnet_private_name
    environment = var.environment
  }
}

main.tf

resource "aws_instance" "this" {
  count                  = var.number_ec2
  ...
  user_data              = var.user_data
  ...
}

...

resource "aws_lb_target_group" "this" {
  ...

  dynamic "health_check" {
    for_each = [var.health_check]

    content {
      interval            = health_check.value.interval
      path                = health_check.value.path
      port                = health_check.value.port
      healthy_threshold   = health_check.value.healthy_threshold
      unhealthy_threshold = health_check.value.unhealthy_threshold
      timeout             = health_check.value.timeout
    }
  }

  depends_on = [aws_lb.this]

}

resource "aws_lb_target_group_attachment" "this" {
  count            = var.number_ec2
  ...
}

Dois novos arquivos foram criados, o versions.tf, que vai conter as informações das versos básicas que o terraform precisa,

versions.tf

terraform {
  required_version = ">= 1.0.0"

  required_providers {
    aws = ">= 3.46"
  }
}

e o outputs.tf que tem informações de alguns recursos.

outputs.tf

output "lb_dns_name" {
  description = "DNS do load balancer."
  value       = concat(aws_lb.this.*.dns_name, [""])[0]
}

Em resumo, adicionamos mais variáveis, para que nossos resources fiquem o mais dinâmico possível e possamos usar ele em outros projetos. Para usar qualquer módulo, você precisa referenciar o código que vai usar(source),e adicionar os provider, para isso, vamos criar uma pasta nova, e adicionar nosso módulos.

$ mkdir /your/path/app-homer
$ cd /your/path/app-homer
$ vi main.tf

Dentro do arquivo main.tf, vamos adicionar o seguinte código:

provider "aws" {
  region                  = "us-east-1"
  shared_credentials_file = "$HOME/.aws/credentials"
}


terraform {
  backend "s3" {
    bucket = "you-bucket"
    key    = "homer/terraform.tfstate"
    region = "us-east-1"
  }
}

module "teste_homer" {
  source = "git::https://github.com/adrianocanofre/homer.git?ref=v0.0.1"

  app_name  = "teste-homer"
  user_data = file("scripts/userdata.sh")
}

A novidade nesse momento é o resource module, nele estou passando o path do modulo que vou usar, e algumas variáveis de entrada. Caso você queira, pode mudar o valor do source e adicionar o path do seu modulo.

Após isso, vamos executar nosso modulo.

$ terraform init
$ terraform plan
$ terraform apply

Para validar que está tudo ok, basta pega o DNS e fazer um curl para testar e validar.

Resumindo, adicionamos mais variáveis, para que nossos resources sejam os mais dinâmicos possíveis e possamos usá-los em outros projetos. Para usar qualquer módulo, você precisa referenciar o código fonte(source) que deseja usar e adicionar um provider, para isso, vamos criar uma nova pasta e adicionar nosso módulo.

Obs Lembre-se de destruir sua infra após terminar de usar ela.

$ terraform destroy --auto-approve

A versâo final desse post, você encontra aqui.

Caso tenha curtindo, deixa seu feedback e compartilhe com os colegas!!