Terraform Modules: Porquê E Como Trabalhar Com Módulos No Terraform - PARTE 1
Todos sabem o quão fantástico o Terraform é quando o assunto é criar infraestrutura de forma automática e através de código, a chamada IaC ou Infraestrutura como Código (Infrastructure as Code). No entanto, nem todos se utilizam de todos os recursos ou funcionalidades que o Terraform nos disponibiliza para tornar nossa vida ainda mais fácil e nosso código mais organizado.
A possibilidade de utilizar módulos em nosso código Terraform faz com que possamos ter a reutilização de código, não apenas nosso mas também de terceiros, evitando a repetição de código bem como nos dando flexibilidade para criarmos dezenas de recursos similares porém com suas respectivas particularidades utilizando-se da mesma base de código.
O objetivo deste post não é ensinar o básico de Terraform, portanto se você ainda não possui uma certa familiaridade com o Terraform, sugiro que volte um pouco e leia estes posts antes de seguir com esta leitura:
- Infraestrutura como Código com Terraform
- Introdução ao Terraform
- Terraform: Variáveis e Outputs
- Terraform: Criando uma infraestrutura no Google Cloud
Multi Plataforma - AWS
Já que o Terraform é multi plataforma, desta vez utilizarei o AWS como cloud para criar minha infraestrutura, ao contrário dos posts anteriores nos quais utilizei Google Cloud.
Como costumo deixar claro, gosto muito de ambas as opções, GCP (Google Cloud Plataform) e AWS (Amazon Web Services), e não gosto de julgar uma como melhor que a outra. GCP se sai melhor em alguns aspectos, enquanto que AWS se sai melhor em outros, mas isto não é tema para este post.
Caso queira acompanhar o passo a passo deste post e praticar os mesmos códigos, você precisará atender a alguns pré-requisitos:
- Já possuir uma conta no AWS. O AWS lhe permite criar uma conta para que você utilize alguns recursos gratuitamente durante os primeiros 12 meses para fins de estudos e serão estes os recursos nos quais focarei aqui;
- Possuir um usuário não root com sua devida chave. Por padrão, ao criar uma conta no AWS, você sempre logará inicialmente com a conta root ou master. Embora muitas pessoas utilizem esta conta para tudo, é extremamente importante, por questões de segurança, que esta conta seja utilizada única e exclusivamente para criar novos usuários. Portanto, sugiro que você crie um novo usuário qualquer bem como uma chave para o mesmo, de forma que você possa interagir com o AWS programaticamente, via APIs, aws cli ou mesmo via Terraform. Você deverá possuir o AWS_ACCESS_KEY_ID deste usuário, bem como sua AWS_SECRET_ACCESS_KEY. Estas informações serão utilizadas para que você consiga se comunicar com o AWS através do Terraform;
Criando uma conta e usuário no AWS
Caso você já possua uma conta e um usuário pronto para utilização, siga para a próxima sessão: Criando VMs no AWS com Terraform
Para criar sua conta no AWS preencha o formulário de cadastro através do seguinte link: Cadastro AWS
Após ter sua conta criada, efetue login com seu email para que possamos criar um usuário para nosso exemplo:
No canto superior esquerdo clique em Serviços (Services). Busque e clique no serviço IAM. No menu da lateral esquerda clique em Usuários (Users) e em seguida clique no botão azul Adicionar Usuário (Add User). Insira um nome para seu usuário e, em Tipo de Acesso (Access Type), selecione a opcão Acesso programático (Programmatic Access). Clique no botão azul do canto inferior direito Próximo: Permissões (Next: Permissions).
Na tela seguinte clique em Anexar políticas existentes de forma direta (Attach existing policies directly) e em seguida, no campo de busca, digite AmazonEC2FullAccess. Selecione a caixa para atribuir esta policy ao seu novo usuário.
Agora que demos permissão para criar instâncias EC2 (VMs) ao nosso usuário, clique no botão azul Próximo: Tags (Next: Tags).
Embora Tags não sejam obrigatórias, por questões de organização e para seguirmos melhores práticas, definiremos uma Tag para este usuário: Uso: Terraform
Clique no botão azul do canto inferior direito, Próximo: Revisar (Next: Review).
Certifique-se de que está tudo certo quanto ao seu novo usuário e em seguida clique no botão azul Criar Usuário (Create user).
ATENÇÃO! Tenha cuidado nesta tela seguinte, pois esta será a única vez em que a senha de sua chave será apresentada. Clique no botão Fazer download .csv (Download .csv) para realizar o download de um arquivo .csv com sua chave e senha, ou apenas copie o conteúdo exibido na tela para sua chave e senha após clicar em Exibir (Show) no campo de senha da chave e salve estes dados em algum local seguro, pois você precisará destes dois valores para controlar sua conta AWS programaticamente com este usuário.
Com seu usuário criado, clique no botão do canto inferior direito Fechar (Close).
Criando VMs no AWS com Terraform
Assumindo que você já possui uma conta no AWS e um usuário com chave para acesso, vamos exportar nossa chave de acesso e nossa senha para esta chave. No Linux ou OS X, execute:
$ export AWS_ACCESS_KEY_ID=<SUA_CHAVE_AUI>
$ export AWS_SECRET_ACCESS_KEY=<SUA_SENHA_PARA_A_CHAVE_AQUI>
Você poderá confirmar que seus comandos deram certo digitando:
$ echo $AWS_ACCESS_KEY_ID && echo $AWS_SECRET_ACCESS_KEY
Caso sua chave e senha tenham sido exibidas, podemos partir para o Terraform.
Novamente, estou assumindo que você já possui um conhecimento básico de Terraform, visto que o foco deste post não é explicar detalhadamente o funcionamento do mesmo, mas sim explicar como e porque utilizar módulos no Terraform.
Comecemos criando uma instância ou VM Ubuntu Linux simples.
Iniciemos criando um diretório para nosso projeto e em seguida indo para dentro do mesmo com algum terminal ou console:
$ mkdir terraform
$ cd terraform
A forma mais simplificada de iniciar nosso projeto e criar uma simples instância Ubuntu seria criando um arquivo main.tf e inserindo o seguinte conteúdo no mesmo:
|
|
Obviamente este código é extremamente simplista. Sequer estamos definindo variáveis, mas chegaremos lá.
O que fizemos:
- Indicamos que o nosso provider será o AWS;
- Criamos um resource do tipo aws_instance;
- Indicamos qual o ID exato da imagem que queremos utilizar para nossa instância. A lista completa de AMI ou imagens disponibilizadas pelo AWS pode ser encontrada aqui;
- Indiquei o tipo de instância como sendo t2.micro. Cada tipo de instância representa uma configuração diferente em termos de CPU, memória, etc. Uma lista completa com todos os tipos disponíveis pode ser encontrada aqui;
- Inseri algumas tags para seguir melhores práticas e deixar nossa infraestrutura organizada e catalogada;
Primeiramente, vamos testar nosso código e ver se estamos com acesso ao AWS via Terraform.
Vamos validar que não temos esta instância já criada no AWS clicando em Serviços (Services) e em seguida buscando o serviço EC2:
Em seguida clique em Instâncias (Instances) no menu esquerdo para listar suas instâncias:
De volta ao nosso terminal, no diretório onde criamos nosso arquivo main.tf, vamos iniciar o terraform com terraform init . :
$ terraform init .
Initializing provider plugins...
The following providers do not have any version constraints in configuration,
so the latest version was installed.
To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.
* provider.aws: version = "~> 2.11"
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
Caso você não tenha recebido um erro com o init, podemos executar o nosso plano ou plan:
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
+ aws_instance.server1
id: <computed>
ami: "ami-0a313d6098716f372"
arn: <computed>
associate_public_ip_address: <computed>
availability_zone: <computed>
cpu_core_count: <computed>
cpu_threads_per_core: <computed>
ebs_block_device.#: <computed>
ephemeral_block_device.#: <computed>
get_password_data: "false"
host_id: <computed>
instance_state: <computed>
instance_type: "t2.micro"
ipv6_address_count: <computed>
ipv6_addresses.#: <computed>
key_name: <computed>
network_interface.#: <computed>
network_interface_id: <computed>
password_data: <computed>
placement_group: <computed>
primary_network_interface_id: <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>
tags.%: "2"
tags.Ambiente: "Desenvolvimento"
tags.Name: "Ubuntu-1"
tenancy: <computed>
volume_tags.%: <computed>
vpc_security_group_ids.#: <computed>
Plan: 1 to add, 0 to change, 0 to destroy.
------------------------------------------------------------------------
Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.
Tudo parece certo no plano, portanto o próximo passo será aplicarmos este código:
$ terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
+ aws_instance.server1
id: <computed>
ami: "ami-0a313d6098716f372"
arn: <computed>
associate_public_ip_address: <computed>
availability_zone: <computed>
cpu_core_count: <computed>
cpu_threads_per_core: <computed>
ebs_block_device.#: <computed>
ephemeral_block_device.#: <computed>
get_password_data: "false"
host_id: <computed>
instance_state: <computed>
instance_type: "t2.micro"
ipv6_address_count: <computed>
ipv6_addresses.#: <computed>
key_name: <computed>
network_interface.#: <computed>
network_interface_id: <computed>
password_data: <computed>
placement_group: <computed>
primary_network_interface_id: <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>
tags.%: "2"
tags.Ambiente: "Desenvolvimento"
tags.Name: "Ubuntu-1"
tenancy: <computed>
volume_tags.%: <computed>
vpc_security_group_ids.#: <computed>
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_instance.server1: Creating...
ami: "" => "ami-0a313d6098716f372"
arn: "" => "<computed>"
associate_public_ip_address: "" => "<computed>"
availability_zone: "" => "<computed>"
cpu_core_count: "" => "<computed>"
cpu_threads_per_core: "" => "<computed>"
ebs_block_device.#: "" => "<computed>"
ephemeral_block_device.#: "" => "<computed>"
get_password_data: "" => "false"
host_id: "" => "<computed>"
instance_state: "" => "<computed>"
instance_type: "" => "t2.micro"
ipv6_address_count: "" => "<computed>"
ipv6_addresses.#: "" => "<computed>"
key_name: "" => "<computed>"
network_interface.#: "" => "<computed>"
network_interface_id: "" => "<computed>"
password_data: "" => "<computed>"
placement_group: "" => "<computed>"
primary_network_interface_id: "" => "<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>"
tags.%: "" => "2"
tags.Ambiente: "" => "Desenvolvimento"
tags.Name: "" => "Ubuntu-1"
tenancy: "" => "<computed>"
volume_tags.%: "" => "<computed>"
vpc_security_group_ids.#: "" => "<computed>"
aws_instance.server1: Still creating... (10s elapsed)
aws_instance.server1: Still creating... (20s elapsed)
aws_instance.server1: Still creating... (30s elapsed)
aws_instance.server1: Creation complete after 34s (ID: i-0cc1dcdbee6a81644)
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Ótimo. Nenhum erro e um resource criado. Se atualizarmos a lista de instâncias em nossa interface do AWS, deveremos ver nossa nova instância Ubuntu criada.
Tudo certo. Nossa instância foi criada e está disponível para acesso.
Mas tudo ainda está muito simples e desorganizado. Vamos destruir a nossa infraestrutura para melhorarmos um pouco. Utilizaremos terraform destroy para destruir tudo o que foi criado:
$ terraform destroy
aws_instance.server1: Refreshing state... (ID: i-0cc1dcdbee6a81644)
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
- destroy
Terraform will perform the following actions:
- aws_instance.server1
Plan: 0 to add, 0 to change, 1 to destroy.
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
aws_instance.server1: Destroying... (ID: i-0cc1dcdbee6a81644)
aws_instance.server1: Still destroying... (ID: i-0cc1dcdbee6a81644, 10s elapsed)
aws_instance.server1: Still destroying... (ID: i-0cc1dcdbee6a81644, 20s elapsed)
aws_instance.server1: Destruction complete after 21s
Destroy complete! Resources: 1 destroyed.
Antes de mais nada, vamos organizar o código e criar algumas variáveis.
Comecemos criando um arquivo chamado variables.tf no mesmo diretório onde se encontra o arquivo main.tf e insira as seguintes variáveis nele:
|
|
Agora que temos nosso arquivo de variáveis, vamos limpar nosso arquivo main.tf para que ele faça uso destas variáveis, eliminando assim valores codificados diretamente em nosso arquivo principal. O arquivo main.tf deverá ficar assim:
|
|
Embora tenhamos organizado melhor nosso código, nada de fato mudou no resultado.
Sinta-se livre para rodar terraform plan e terraform apply em seguida para constatar que ele apenas criará a mesma instância novamente, caso você tenha destruído a anterior com terraform destroy, claro.
Após testar, lembre-se de limpar sua infraestrutura novamente com terraform destroy, pois estamos apenas testando nosso código por enquanto.
Até então temos apenas o código que cria uma instância Ubuntu comum e vazia. Agora que temos este código pronto, a gerência da empresa na qual você trabalha lhe pede que crie um projeto chamado Webserver. Tudo o que este projeto precisa ter por enquanto é uma instância Ubuntu. Ela não precisa possuir de fato um servidor web instalado neste momento.
Você é esperto e já tinha o código pronto, apenas não possuia um projeto para utilizá-lo. Neste caso, vamos começar a organizar nossos projetos. O que temos até então é um diretório qualquer, que no meu caso se chama terraform, e dentro dele temos dois arquivos principais:
- main.tf
- variables.tf
Além deles, o Terraform terá criado alguns arquivos de state onde ele armazena as informações sobre o estado de sua infraestrutura, bem como um diretório oculto .terraform.
Vamos criar um diretório para nosso novo projeto. O novo diretório deverá se chamar webserver. Este deverá ser criado dentro do seu diretório original. Além disso, agora podemos mover nossos arquivos para dentro de nosso diretório de projeto. Sua estrutura de diretórios e arquivos agora deverá ser similar a esta: (Os arquivos realmente importantes são main.tf e variables.tf, visto que como destruimos nossa infraestrutura por completo, podemos facilmente ter novos arquivos de state.)
terraform
└── webserver
├── main.tf
├── terraform.tfstate
├── terraform.tfstate.backup
└── variables.tf
Imagine que além do projeto webserver, agora você precisará se preparar para o projeto que rodará um banco de dados em uma instância também Ubuntu. Por enquanto tudo o que será preciso fazer é criar um diretório para o projeto db e criar o código que criará uma instância Ubuntu.
Como já temos o código que faz isso, podemos simplesmente copiar os mesmos dois arquivos main.tf e variables.tf para dentro de um novo diretório chamado db e alterar alguns valores neles, certo?!
Sendo assim, nossa árvore de diretórios agora estará assim:
terraform
└── db
├── main.tf
├── variables.tf
└── webserver
├── main.tf
├── terraform.tfstate
├── terraform.tfstate.backup
└── variables.tf
Como já tínhamos isolado nossas variáveis, não precisaremos tocar nos arquivos main.tf. Podemos alterar apenas valores das variáveis desejadas nos respectivos arquivos variables.tf.
Começando pelo variables.tf do projeto webserver, alteraremos apenas a variável de tags, que agora ficará assim:
|
|
Alteramos apenas o nome da instância para Ubuntu-webserver e adicionamos uma tag para o projeto.
Agora faremos algo similar no arquivo variables.tf do projeto db, ficando assim:
|
|
Neste momento não temos nenhuma instância rodando, pois exluímos a que criamos anteriormente com terraform destroy.
Vamos validar nosso código agora. Primeiramente, vamos executar os seguintes comandos de dentro do diretório db:
$ terraform init .
$ terraform plan
$ terraform apply
Em seguida, de dentro do diretório webserver execute também os comandos:
$ terraform plan
$ terraform apply
O motivo pelo qual não precisamos executar terraform init . para o projeto webserver é por já termos feito isso anteriormente, portanto já devemos ter os arquivos de configuração e state do Terraform dentro de nosso diretório webserver. Caso contrário, execute terraform init . antes do plan.
Se verificarmos nossas instâncias no AWS agora, devemos ter as duas instâncias: Ubuntu-webserver e Ubuntu-db.
Já vimos que nossas duas instâncias podem ser criadas com nosso código mas até então elas não fazem nada além de simplesmente existir.
Vamos destruir tudo o que foi criado para não gastarmos nossos créditos AWS de forma desnecessária. Execute terraform destroy a partir de ambos os diretórios, db e webserver para garantir que ambas as instâncias serão destruídas.
Na PARTE 2 deste tutorial criaremos então o código que defato definirá e criará um servidor web e um servidor de banco de dados rapidamente.