Construa um código reutilizável no Terraform com a ajuda de módulos

Adelson Junior
GetNinjas
Published in
9 min readMay 2, 2017

--

Se o título desse texto te chamou atenção, provavelmente, você já sabe o que é IaC (Infraestructure as a Code), faz uso de ferramentas como Chef, Puppet ou Ansible para gerenciamento da stack de provisionamento e como esse ecossistema, se bem implementado, ajuda a manter uma cultura DevOps para garantir uma entrega rápida e com qualidade.

Agora se nunca ouviu falar dessa sopa de letrinhas e tudo que falei até agora não fez o menor sentido, podem seguir com a leitura, mas estamos preparando um artigo bem legal sobre tudo isso que dará uma boa base para uma melhor compreensão deste texto.

Pois bem, neste artigo, queremos apresentar o porque nós da Get Ninjas escolhemos o Terraform como ferramenta de IaC, e de quebra mostrar como estender seu uso, utilizando módulos.

Perae, mas o que é Terraform?

Terraform é uma simples, poderosa e eficiente ferramenta para a criação e gerenciamento da stack de infraestrutura como código.

Com o Terraform, conseguimos criar/descrever toda a infraestrutura usando uma sintaxe declarativa simples, chamada de Hashicorp Configuration Language.

Falando em Hashicorp, ela é a empresa que mantém o Terraform, assim como várias outras ferramentas bem conhecidas no contexto DevOps: Packer, Consul, Vault e Vagrant.

Aqui na GetNinjas todo e qualquer ambiente novo está sendo criado utilizando o Terraform.

Por conta disso, fizemos uma Tech Talk hands on com nossos Devs, onde eles puderam criar alguns recursos e quebrar a cabeça um pouco.

Nessa Talk, uma das maiores dúvidas do pessoal era porque utilizar o Terraform e não a ferramenta de Provisionamento que já utilizamos, para a criação da infra, ou seja, porque não utilizamos, por exemplo, o Chef?

Para ficar mais claro onde o Terraform entra, vamos compará-lo à algumas outras ferramentas bem conhecidas no mundo devops.

Terraform Vs. Chef / Puppet / Ansible

Terraform não é uma Configuration management tool ou ferramenta de gerenciamento de configuração. Seu foco é a criação dos recursos / infra, enquanto que nessas ferramentas o foco é o bootstraping, ou seja, uma vez que a infra foi criada, essas ferramentas entram em cena para provisionar (instalar e configurar) o ambiente.

Terraform vs. CloudFormation

O CloudFormation da AWS, permite descrever e codificar a infraestrutura por meio de arquivos de configuração, permitindo que os recursos sejam criados, modificados e até destruídos. O Terraform nasceu com este objetivo em mente. Indo mais além: é multi provider. Tem suporte tanto para aws, Google Cloud, Azure e outros providers, como Cloudflare, SimpleDNS e vários outros.

Terraform vs. Boto

Boto é uma biblioteca python que traz uma interface de baixo nível com a API da AWS. Para utilizarmos, precisamos codificar, ou seja, desenvolver uma aplicação/script para criar a infraestrutura. O Terraform nos dá uma linguagem de alto nível, com uma sintaxe que nos permite descrever exatamente a nossa infraestrutura e quais os recursos que serão criados, modificados ou destruídos.

Por que Terraform?

Os principais motivos que nos fizeram decidir pelo Terraform foram:

  1. Ter a infraestrutura como código.
  2. Ser multi provider (podemos criar tanto para AWS, como Google Cloud e Azure)
  3. Curva de aprendizagem relativamente baixa.
  4. Sintaxe declarativa.
  5. Suporte à gama de serviços AWS gigantesca.

Começando

Para começar a brincar com o Terraform, é muito simples:

Faça o download na página oficial e instale de acordo com o seu sistema operacional.

Após instalado, você pode verificar a versão com umterraform -v

Terraform v0.9.2

Para começar a criar seus recursos, crie um arquivo chamado example.tf e coloque o seguinte conteúdo (abaixo) nele. Atente-se colocar as suas credenciais da AWS.

Uma outra forma, inclusive mais segura e recomendada, é setar as variáveis de ambiente na sua sessão shell. Dessa forma, as credenciais não ficam expostas em arquivos.

export AWS_ACCESS_KEY_ID="access_key_here"export AWS_SECRET_ACCESS_KEY="secret_key_here"

Após configurado as credenciais, rode um terraform plan

...
+ aws_instance.example
ami: "ami-c80b0aa2"
associate_public_ip_address: "<computed>"
availability_zone: "<computed>"
ebs_block_device.#: "<computed>"
ephemeral_block_device.#: "<computed>"
instance_state: "<computed>"
instance_type: "t2.micro"
ipv6_addresses.#: "<computed>"
key_name: "<computed>"
network_interface_id: "<computed>"
placement_group: "<computed>"
private_dns: "<computed>"
private_ip: "<computed>"
public_dns: "<computed>"
public_ip: "<computed>"
root_block_device.#: "<computed>"
security_groups.#: "<computed>"
source_dest_check: "true"
subnet_id: "<computed>"
tenancy: "<computed>"
vpc_security_group_ids.#: "<computed>"
Plan: 1 to add, 0 to change, 0 to destroy.

O terraform plan nos mostra quais as modificações que o terraform irá executar.

Para aplicar e criar esta instância ec2 faça um terraform apply

aws_instance.example: Creating…
ami: “” => “ami-c80b0aa2”
associate_public_ip_address: “” => “<computed>”
availability_zone: “” => “<computed>”
ebs_block_device.#: “” => “<computed>”
ephemeral_block_device.#: “” => “<computed>”
instance_state: “” => “<computed>”
instance_type: “” => “t2.micro”
ipv6_addresses.#: “” => “<computed>”
key_name: “” => “<computed>”
network_interface_id: “” => “<computed>”
placement_group: “” => “<computed>”
private_dns: “” => “<computed>”
private_ip: “” => “<computed>”
public_dns: “” => “<computed>”
public_ip: “” => “<computed>”
root_block_device.#: “” => “<computed>”
security_groups.#: “” => “<computed>”
source_dest_check: “” => “true”
subnet_id: “” => “subnet-e75d10be”
tenancy: “” => “<computed>”
vpc_security_group_ids.#: “” => “<computed>”
aws_instance.example: Still creating… (10s elapsed)
aws_instance.example: Still creating… (20s elapsed)
aws_instance.example: Creation complete (ID: i-0e2cc4bed29c4a86d)
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Instância criada! Temos o recurso criado no nosso provider aws e o código que o descreve.

Podemos ver o metadata do recurso criado (a ec2) usando o terraform show.

Estendendo a infra

Vamos agora criar um Security Group e um ELB para ser usado com esta nossa instância para exemplificar como podemos estender a criação dos recursos.

Crie mais dois arquivos no diretório, um com o nome de elb.tf e o outro sg.tf. Os códigos estão aqui abaixo:

Uma novidade aqui! Nestes arquivos estamos fazendo uso de variáveis, como ${var.name} ou ${var.environment} para exemplificar.

Para setá-las, crie um arquivo chamado variables.tf com o seguinte conteúdo:

É neste arquivo que definimos as variáveis, quais são seus valores default e o tipo (string, list, map, etc). Aqui, estamos criando duas: name e environment.

Vamos chamá-las dentro dos arquivos *.tf onde a descrição e a lógica de criação do recurso são executadas. A sintaxe para a chamar as variáveis dentro dos arquivos *.tf é:

${var.<nome da variável>}

Neste caso, o terraform irá pegar o valor declarado como default no arquivo. Para sobrescrever o valor, nós podemos declarar a variável em tempo de execução (tanto no plan, como no apply), por exemplo, alterando a variável "name" como no comando a seguir:

terraform plan -var name=MyGreatApp

Agora rode umterraform plan para ver as modificações.

+ aws_elb.elb
availability_zones.#: “<computed>”
connection_draining: “false”
connection_draining_timeout: “300”
cross_zone_load_balancing: “true”
dns_name: “<computed>”
health_check.#: “1”
health_check.0.healthy_threshold: “2”
health_check.0.interval: “5”
health_check.0.target: “TCP:80”
health_check.0.timeout: “3”
health_check.0.unhealthy_threshold: “2”
idle_timeout: “60”
instances.#: “<computed>”
internal: “<computed>”
listener.#: “1”
listener.2974294026.instance_port: “80”
listener.2974294026.instance_protocol: “tcp”
listener.2974294026.lb_port: “80”
listener.2974294026.lb_protocol: “tcp”
listener.2974294026.ssl_certificate_id: “”
name: “example”
security_groups.#: “<computed>”
source_security_group: “<computed>”
source_security_group_id: “<computed>”
subnets.#: “1”
subnets.1732792387: “subnet-e75d10be”
tags.%: “2”
tags.Environment: “staging”
tags.Name: “example”
zone_id: “<computed>”
-/+ aws_instance.example
ami: “ami-c80b0aa2” => “ami-c80b0aa2”
associate_public_ip_address: “false” => “<computed>”
availability_zone: “us-east-1d” => “<computed>”
ebs_block_device.#: “0” => “<computed>”
ephemeral_block_device.#: “0” => “<computed>”
instance_state: “running” => “<computed>”
instance_type: “t2.micro” => “t2.micro”
ipv6_addresses.#: “0” => “<computed>”
key_name: “” => “<computed>”
network_interface_id: “eni-77d56fae” => “<computed>”
placement_group: “” => “<computed>”
private_dns: “ip-10–0–63–209.ec2.internal” => “<computed>”
private_ip: “10.0.63.209” => “<computed>”
public_dns: “” => “<computed>”
public_ip: “” => “<computed>”
root_block_device.#: “1” => “<computed>”
security_groups.#: “0” => “<computed>” (forces new resource)
source_dest_check: “true” => “true”
subnet_id: “subnet-e75d10be” => “subnet-e75d10be”
tenancy: “default” => “<computed>”
vpc_security_group_ids.#: “1” => “<computed>”
+ aws_security_group.sg
description: “Allow all inbound traffic”
egress.#: “1”
egress.1403647648.cidr_blocks.#: “1”
egress.1403647648.cidr_blocks.0: “0.0.0.0/0”
egress.1403647648.from_port: “0”
egress.1403647648.ipv6_cidr_blocks.#: “0”
egress.1403647648.prefix_list_ids.#: “0”
egress.1403647648.protocol: “tcp”
egress.1403647648.security_groups.#: “0”
egress.1403647648.self: “false”
egress.1403647648.to_port: “65535”
ingress.#: “2”
ingress.2214680975.cidr_blocks.#: “1”
ingress.2214680975.cidr_blocks.0: “0.0.0.0/0”
ingress.2214680975.from_port: “80”
ingress.2214680975.ipv6_cidr_blocks.#: “0”
ingress.2214680975.protocol: “tcp”
ingress.2214680975.security_groups.#: “0”
ingress.2214680975.self: “false”
ingress.2214680975.to_port: “80”
ingress.2541437006.cidr_blocks.#: “1”
ingress.2541437006.cidr_blocks.0: “0.0.0.0/0”
ingress.2541437006.from_port: “22”
ingress.2541437006.ipv6_cidr_blocks.#: “0”
ingress.2541437006.protocol: “tcp”
ingress.2541437006.security_groups.#: “0”
ingress.2541437006.self: “false”
ingress.2541437006.to_port: “22”
name: “example-sg”
owner_id: “<computed>”
Plan: 3 to add, 0 to change, 1 to destroy.

Este plan está dizendo que o Terraform criará outros 2 recursos (elb e security group) e nossa instância EC2 que será recriada (note o sinal "-/+").

Execute um terraform apply e voilà, seus recursos serão criados!

Saindo do básico

Até aqui nós descrevemos nossa pequena infra composta de uma EC2, um security group e um elb, utilizando a sintaxe HCL do Terraform. Poderíamos criar tudo em um só arquivo .tf, mas optamos por dividir em mais arquivos (por mera convenção). Essa pequena infra pode ser uma API ou uma interface Web, já que expõe via security group a porta 80 e está atrás de um load balancer.

Agora, vamos imaginar o seguinte cenário:

Esta infra foi criada para suprir uma demanda da equipe de produto e a aplicação que está rodando na sua infra deu tão certo, que ela precisa ser criada mais 3 vezes para outros setores, com os mesmos recursos, ou seja, 1 ec2, 1 elb e 1 security group.

O que você faria? Repetiria o código acima apenas mudando as variáveis?

Não repita código!

Crie seus próprios módulos e reutilize-os.

Criando módulos no Terraform

Módulos são pacotes independentes de configuração que podem ser agrupados de acordo com alguma finalidade específica. É uma forma de criar componentes que podem ser reutilizados.

Vamos pegar o nosso probleminha exposto aí acima.

Precisamos criar 3 ambientes idênticos, apenas alterando seus "donos".

Ao invés de criarmos a infra logo de cara, criaremos um módulo que descreve este tipo de infraestrutura, que é composta por uma ec2, um elb e um security group e vamos "gitar" este módulo no github e usá-lo para criar a nossa infra.

Para criar um módulo básico, fazemos uso da seguinte estrutura de arquivos:

O nome do módulo é "web-site-module-example"
  • README: Dispensa apresentações, né? Aqui o ideal é colocar quais as variáveis que o seu módulo faz uso e aceita.
  • main.tf: É o arquivo principal do módulo. A lógica dos recursos é criada neste arquivo. No nosso exemplo é nele que está a lógica de criação da EC2, do ELB e do Security Group
  • variables.tf: Arquivo contendo todas as variáveis que o seu módulo faz uso e espera.

Seguem os arquivos e o código:

Ou faça o clone deste projeto, que já vem com todos os arquivos do módulo.

git clone git@github.com:adelsjnr/web-site-module-example.git

Agora, crie um diretório, por exemplo, web-app e dentro dele crie os respectivos arquivos .tf, um para cara "setor". Vamos supor que temos os setores de facilities, finance e marketing. A estrutura de diretórios ficará assim:

E aqui o código de cada arquivo:

O que estamos fazendo aqui?

Estamos criando o recurso fazendo uso do módulo previamente criado. Utilizamos a diretiva source para dizer ao Terraform onde ele irá buscar o módulo, ou seja, toda a lógica a ser utilizada para criar os recursos e apenas nos preocupamos com as variáveis a serem exportadas.

Neste caso, o módulo está localizado em outro diretório, mas pode ser um repositório no Github, Bitbucket (com repo publico ou privado), um bucket s3 e até um endpoint HTTP. Por exemplo, para usar este modulo diretamente do repositório, altere a diretiva source, ficando assim:

Ao fazer uso de módulos, antes de executarmos os famosos terraform plane terraform apply precisamos rodar o comando terraform get -update

Agora sim! terraform plane terraform apply

Concluindo: o céu é o limite.

Bem, é isso! Fazendo uso de módulos, além de não precisar repetir código, ganhamos um baita tempo na criação dos recursos, já que é só importar, declarar as variáveis, terraform get, plan e apply!

Para se ter uma ideia, aqui fazemos muito uso de Autoscale Groups para rodar os ambientes. Este autoscale faz uso de roles, load balancer, security group e mais uma gama de recursos aws. Então, se para cada aplicação tivéssemos que re-escrever o código seria bem trabalhoso, ou não, fazendo uso de copy-paste, mas com o uso de módulos podemos centralizar e manter um código limpo.

No próximo artigo vamos mostrar como lidamos com a criação de recursos em time, múltiplos ambientes, remote e locking state. Aguardem.

--

--