Terraform: Criando Uma Infraestrutura No Google Cloud

| Comments

Quando se fala em infraestrutura como código imagina-se algo mais complexo do que simplesmente um container docker rodando uma aplicação, certo?! O propósito deste post é justamente criar uma infraestrutura um pouco mais complexa e completa no GCP, Google Cloud Platform. Embora o Terraform possua integração com diversos provedores de computação em nuvem, utilizarei o Google Cloud para este post por estar trabalhando mais com GCP atualmente e estar gostando da experiência.

Uma vez que não entrarei em tantos detalhes básicos do Terraform neste post, espero que você já possua algum conhecimento básico sobre o mesmo bem como entenda como funcionam seus resources, variables, etc. Do contrário, recomendo fortemente que você volte um pouco e leia meus posts anteriores nesta respectiva ordem:

Assumindo que você já possui algum conhecimento básico sobre Terraform, chegou a hora de inciarmos o nosso pequeno projeto de infraestrutura como código.

GCP – Google Cloud Platform

Google Cloud Platform, ou GCP, é uma suíte de computação em nuvem oferecida pelo Google, funcionando na mesma infraestrutura que a empresa utiliza para seus produtos dirigidos aos usuários, dentre eles o buscador Google e o Youtube. Juntamente com um conjunto de ferramentas de gerenciamento modulares, o GCP fornece uma série de serviços incluindo computação, armazenamento de dados, análise de dados, machine learning, containers, etc.

Novamente, o motivo pelo qual optei por utilizar GCP para este pequeno projeto foi simplesmente o fato de eu estar trabalhando mais com GCP em meu dia a dia atual, mas nada impede que você utilize AWS ou Microsoft Azure, por exemplo. Embora a sintaxe e o código Terraform deverá ser ajustado para tais plataformas caso decida utilizá-las.

Outro motivo interessante é o plano gratuito oferecido pelo Google, o que facilita nossos estudos e experimentos. O GCP nos oferece 1 ano de utilização grátis OU $300 dólares em créditos, o que ocorrer primeiro. De uma forma ou de outra, isto será muito mais que o suficiente para a execução deste nosso projeto.

Além das duas razões já citadas, a integração e facilidade de criação de uma nova conta no GCP foi levada em conta para esta escolha. O fato de o gmail e demais serviços do Google serem amplamente utilizados, é bem provável que você possua um email do Google (gmail, por exemplo), certo!? Se este é o caso, você já possui uma “pré-conta” no GCP sem ao menos saber.

Cadastro

Se você possui uma conta do Google, autentique-se com a mesma. Caso não possua uma, você poderá criar uma através do Gmail, por exemplo, ou criar diretamente na interface do Google Cloud durante o cadastro.

1- Uma vez logado com sua conta do Google, acesse em seu navegador o seguinte endereço: https://cloud.google.com ;

2- Caso esteja logado com sua conta do Google, verá sua foto ou imagem de conta no canto superior direito da página conforme na imagem a seguir. Clique em Try free ou Experimente Gratuitamente;

3- Você cairá na primeira página do cadastro. Informe seu País, leia e concorde com os termos de uso caso deseje seguir e em seguida clique em Concordar e Continuar;

4- Após confirmar e validar todas as informações que eles pedem na etapa 2, clique em Iniciar minha avaliação gratuita;

Em poucos segundos você deverá cair no painel ou dashboard principal do GCP, com um popup de boas vindas com alguma mensagem de boas vindas: “Olá, Marcelo, Agradecemos por você se inscrever na avaliação gratuita de 12 meses. Demos US$ 300 de crédito grátis para você gastar. Não se preocupe se o crédito acabar, você só receberá cobranças se tivermos sua permissão.”

Este é outro fator interessante do GCP. Durante este período de avaliação, você não corre o risco de ser cobrado caso utilize mais do que deveria por descuido. Uma vez que o período de 12 meses, ou o crédito de $300 tenha se esgotado, você será notificado e deverá encerrar sua conta ou confirmar que deseja continuar utilizando os serviços, autorizando assim o Google a lhe efetuar cobranças a partir deste momento.

O painel principal se parecerá com este:

Conforme a imagem abaixo, através do Menu principal que se encontra na lateral esquerda, clique em IAM e Admin, e em seguida em Gerenciar recursos.

Você receberá uma listagem inicial de seus recursos. Por padrão, quando se cria uma nova conta no GCP, apenas um projeto inicial é criado, chamado My First Project, sem qualquer recurso vinculado ou inserido no mesmo.

Criando uma conta de serviço ou service account

Embora seja possível e permitido utilizar-se de uma conta pessoal para este tipo de tarefa, não é o mais indicado. O ideal é deixar serviços utilizarem contas específicas, as chamadas service accounts. Uma vez que utilizaremos o Terraform para criar nossos recursos na nuvem, ele não deixa de funcionar como um serviço, não sendo uma pessoa XYZ de um departamento qualquer de uma empresa.

1- No menu principal da lateral esquerda, clique em IAM e Admin, em seguida em Contas de serviço, conforme na imagem abaixo:

2- Na tela de Contas de serviço, clique em Criar conta de serviço;

3- Indique o nome terraform para esta conta, conforme imagem abaixo, e clique em CRIAR;

4- No passo 2, daremos um papel para esta conta de serviço. Aqui indicaremos quais permissões ela terá. Para facilitar nosso exercício, utilizaremos Projeto > Proprietário, significando que nossa conta de serviço terá todas as permissões em nosso projeto, podendo criar ou destruir qualquer tipo de recurso.

5- Em seguida, clicaremos em Continuar para seguir com a criação de nossa conta de serviço;

6- Na tela seguinte lhe será dada a opção de dar permissões para algum usuário que possa precisar utilizar-se desta conta de serviço para criar recursos. Você pode inserir neste campo o seu usuário principal do google cloud, o qual será o seu email que foi utilizado para criar esta conta no GCP. Insira-o em Papel de usuários da conta de serviço. Em seguida, clique no botão de Criar Chave que se encontra logo abaixo. Esta será a chave criptografada que utilizaremos para comunicação segura com o GCP;

7- Lhe será perguntado em que formato você deseja slavar a chave. Escolha o formato JSON e clique em CRIAR;

8- Escolha com atenção o local onde salvará esta chave, pois você não poderá baixá-la novamente e precisaremos da mesma posteriormente;

OBS: Para facilitar este exemplo, estarei salvando a chave no mesmo diretório no qual criaremos o código de nosso projeto. Caso você decida fazer o mesmo, e decida hospedar este código em alguma espécie de repositório git, por exemplo, lembre-se de sempre incluir esta e outras chaves ou dados sigilosos em seu .gitignore, para que este tipo de arquivo com informações confidenciais não seja enviado para o repositório juntamente com o código. =) (Sim, já vi pessoas hospedando chaves em repositórios git e tendo sérios problemas. Sim, é você mesmo. :p)

Google Cloud SDK

Um dos nossos pré-requisitos será o Google Cloud SDK, que possui um conjunto de ferramentas via linha de comando que nos permitem interagir com o Google Cloud remotamente através de APIs. O Terraform também fará uso desta ferramenta.

Para o funcionamento do gcloud SDK precisaremos também possuir o Python instalado, na versão 2.7. Você poderá confirmar a sua versão do python executando python -V em algum console ou terminal.

Informações detalhadas sobre como instalar o Gcloud SDK encontram-se com excelentes detalhes nas páginas oficiais do Google, listadas abaixo:

É importante lembrar de reiniciar o seu terminal ou console após a instalação.

Para certificar-se de que a instalação foi realizada com sucesso, execute:

1
2
3
4
5
6
$ gcloud --version

Google Cloud SDK 225.0.0
bq 2.0.37
core 2018.11.09
gsutil 4.34

Se você recebeu informações referentes à versão do Google Cloud SDK, significa que a instalação foi bem sucedida. O próximo passo é autenticar-se com sua conta do google através do SDK. Execute gcloud init. O seu navegador deverá abrir automaticamente lhe pedindo a autenticação de sua conta do Google após você confirmar com um Y a solicitação no console ou terminal.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ gcloud init

Welcome! This command will take you through the configuration of gcloud.

Your current configuration has been set to: [default]

You can skip diagnostics next time by using the following flag:
  gcloud init --skip-diagnostics

Network diagnostic detects and fixes local network connection issues.
Checking network connection...done.
Reachability Check passed.
Network diagnostic passed (1/1 checks passed).

You must log in to continue. Would you like to log in (Y/n)?

Caso ao inserir um Y, o seu navegador não lhe solicite a autenticação do Google, copie e cole a longa URL que lhe será apresentada.

A solicitação de autenticação será similar à esta:

Ao finalizar sua autenticação no navegador, você receberá mais uma pergunta em seu terminal ou console. O SDK lhe indicará o seu projeto e lhe perguntará se você quer utilizá-lo ou criar um novo. Vamos escolher a opção 1, para utilizar o projeto que foi criado automaticamente e em seguida confirmar com um Enter:

1
2
3
4
5
6
7
8
9
10
11
12
Your browser has been opened to visit:

    https://accounts.google.com/o/oauth2/auth?reblablabalblablaba=http%3A%2F%2Flocalhost%3A8085%2F&prompt=select_account&response_typereblablabalblablabauid.apps.googleusercontent.com&scope=https%3A%2F%2Fwww.googleapis.com%reblablabalblablabaemreblablabalblablabaw.gooreblablabalblablaba%2Fauth%2Fclreblablabalblablaba%2Fwwwreblablabalblablabacomreblablabalblablabaappenreblablabalblablabattpsreblablabalblablabaww.reblablabalblablabaleapis.com%2Fauthreblablabalblablabattps%3A%2F%2Fwww.googleapis.com%2Fauthreblablabalblablaba&access_type=offline


You are logged in as: [marcelo@marceloemail.net].

Pick cloud project to use:
 [1] possible-sun-meuid
 [2] Create a new project
Please enter numeric choice or text value (must exactly match list
item):

O SDK finalizará o setup e lhe informará que o projeto está pronto para ser utilizado.

Terraform

Vamos ao que interessa agora. Antes de iniciarmos nosso código, crie um diretório chamado terraform-gcp, ou algo de sua preferência. Dentro deste diretório apenas teremos por enquanto o arquivo JSON que baixamos do GCP com as credenciais de nossa conta de serviço.

Criaremos agora nosso primeiro arquivo terraform.

Comecemos criando nosso arquivo de variáveis, o qual por enquanto conterá apenas 2 variáveis para começarmos nosso código. Crie o arquivo variables.tf com o seguinte conteúdo:

variables.tf
1
2
3
4
5
6
7
8
9
variable "project_id" {
  type    = "string"
  default = "possible-sun-83482736"
}

variable "regiao" {
  type = "string"
  default = "northamerica-northeast1"
}

OBS: Lembre-se de alterar o valor default da variável project_id. Eu inseri aqui o id do projeto que foi criado para mim pelo GCP automaticamente. O id do seu projeto deverá ser o mesmo fornecido a você pelo GCP.

Outra coisa importante é lembrar que estou assumindo que você já possui algum conhecimento básico sobre como o Terraform funciona, ou que leu meus posts anteriores sobre o assunto, de forma que não estarei aqui entrando em tantos detalhes explicativos sobre cada arquivo, conforme fiz nos anteriores.

Em nosso arquivo variables.tf, por enquanto, criamos apenas duas variáveis: project_id e regiao. Para quem já utilizou algum serviço de computação em nuvem, seja GCP, AWS, Azure, etc., isto pode soar familiar. Sempre que se deseja criar recursos na nuvem, devemos optar por alguma região disponível no provedor de escolha. Estou optando por utilizar northamerica-northeast1 pelo fato de eu morar em Toronto, e esta ser a região mais próxima, mas sinta-se livre para optar por qualquer região disponível no GCP conforme lista fornecida aqui.

Em seguida criaremos nosso arquivo main.tf que, inicialmente, possuirá apenas o seguinte:

main.tf
1
2
3
4
5
6
# Configura o projeto GCP
provider "google" {
  credentials = "${file("possible-sun-83482736-sabh45jhb2345ghv.json")}"
  project     = "${var.project_id}"
  region      = "${var.regiao}"
}

Aqui estamos apenas informando ao Terraform que utilizaremos o google como provedor ou provider. Para nos conectarmos com o provider precisamos passar nossas credenciais e para tal estamos apontando o arquivo ou file que baixamos do GCP com as informações de nossa conta de serviço. Aqui estou passando apenas o nome do arquivo, pois o mesmo se encontra no mesmo diretório onde se encontra meu código.

Além da credencial, estamos também informando qual o nome do projeto que utilizaremos no GCP bem como a região na qual estaremos trabalhando. Ambos os valores estão sendo trazidos do arquivo variables.tf. Aqui apenas invocamos as variáveis. (Novamente: Se esta invocação das variáveis lhe parece confusa, significa que não leu o post anterior, onde expliquei o básico sobre uso de variáveis no Terraform. Volte uma casa!)

Com ambos os arquivos criados, podemos iniciar a execução de nosso projeto. Neste momento, nosso código não fará nada além de permitir que o Terraform consiga se conectar ao GCP e validar que nossa conta e projeto de fato existem. Executemos terraform init para ver se está tudo certo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ terraform init

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "google" (1.19.1)...

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.google: version = "~> 1.19"

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.

Ao que parece, o nosso state do Terraform foi iniciado com sucesso e tudo parece correto. Vamos tentar criar nosso plano de execução agora com terraform plan:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ 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.


------------------------------------------------------------------------

No changes. Infrastructure is up-to-date.

This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, no
actions need to be performed.

Tudo parece correto e, assim como dito anteriormente, o Terraform também nos informa que tudo está atualizado e que nenhuma ação precisa ser realizada, afinal não indicamos nada a ser criado, nenhum recurso. Apenas indicamos que utilizaremos um determinado projeto em uma determinada região, e tal projeto já existe no GCP.

Vamos então iniciar a criação de nossa infraestrutura básica. Iniciemos criando uma VM simples.

Quando falamos em VMs na nuvem, existem alguns atributos importantes que devemos levar em conta antes de criar a mesma:

  • Nome: Nossa VM precisa ter um nome;
  • Tipo de máquina: Quando se decide comprar um novo servidor para sua empresa, você terá diversas máquinas disponíveis a venda, algumas com mais memória, outras com menos, CPU, Disco, etc. Da mesma forma funcionam VMs em um ambiente de nuvem como o GCP. Precisamos determinar o tipo de VM que queremos;
  • Zona: Todo provedor na nuvem ou cloud possui diversas zonas ou regiões disponíveis espalhadas pelo globo, portanto devemos sempre informar onde queremos rodar nossos recursos;
  • Imagem: Assim como fazemos com um servidor físico, sempre devemos escolher uma imagem ou Sistema Operacional para ser instalado em nossa VM;

Uma vez que tenhamos iuma listagem básica de algumas variáveis importantes para nossa VM, vamos inserí-las em nosso arquivo variables.tf:

variables.tf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
variable "project_id" {
  type    = "string"
  default = "possible-sun-83482736"
}

variable "regiao" {
  type = "string"
  default = "northamerica-northeast1"
}

variable "nome" {
  type = "string"
  default = "vm-webserver"
}

variable "tipo_maquina" {
  type = "string"
  default = "f1-micro"
}

variable "zona" {
  type = "string"
  default = "northamerica-northeast1-a"
}

variable "imagem" {
  type = "string"
  default = "debian-cloud/debian-9"
}

Aqui criamos 4 novas variáveis. Daremos um nome (vm-webserver) para nossa VM, um tipo de máquina (f1-micro), uma zona (em meu caso utilizarei northamerica-northamerica1-a por ser a mais próxima de mim, mas sinta-se livre para utilizar a que melhor lhe servir) e uma imagem (debian-cloud/debian-9, a qual faz parte da enorme lista de imagens disponíveis no Google).

Agora que temos nossas variáveis definidas, vamos editar o arquivo main.tf para criar nossos recursos para a VM:

main.tf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Configura o projeto GCP
provider "google" {
  credentials = "${file("possible-sun-83482736-sabh45jhb2345ghv.json")}"
  project     = "${var.project_id}"
  region      = "${var.regiao}"
}

# Cria a VM com o Google Compute Engine
resource "google_compute_instance" "webserver" {
  name          = "${var.nome}"
  machine_type  = "${var.tipo_maquina}"
  zone          = "${var.zona}"

  boot_disk {
    initialize_params {
      image = "${var.imagem}"
    }
  }

  # Instala o servidor web Apache
  metadata_startup_script = "sudo apt-get update; sudo apt-get install apache2 -y; echo Testando > /var/www/html/index.html"

  # Habilita rede para a VM bem como um IP público
  network_interface {
    network = "default"
    access_config {

    }
  }
}

Aqui inserimos um resource de tipo google_compute_instance para criar nossa VM e nele passamos os detalhes de nossa VM, tais como nome, tipo de máquina, zona e rede.

Estamos também utilizando a propriedade metadata_startup_script, na qual o Terraform nos permite utilizar o recurso de script de inicialização do Google Cloud. Este parâmetro nos permite executar um script logo que a VM é criada, portanto podemos automatizar qualquer setup inicial de nossa VM através deste recurso. Neste exemplo estaremos apenas atualizando os repositórios de nosso Debian e instalando um servidor web Apache para nosso teste.

Agora que temos nossos arquivos variables.tf e main.tf atualizados, vamos executar novamente terraform plan para ver o que aconteceria com nossa infraestrutura:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
$ 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:

 + google_compute_instance.webserver
     id:                                                  <computed>
     boot_disk.#:                                         "1"
     boot_disk.0.auto_delete:                             "true"
     boot_disk.0.device_name:                             <computed>
     boot_disk.0.disk_encryption_key_sha256:              <computed>
     boot_disk.0.initialize_params.#:                     "1"
     boot_disk.0.initialize_params.0.image:               "debian-cloud/debian-9"
     boot_disk.0.initialize_params.0.size:                <computed>
     boot_disk.0.initialize_params.0.type:                <computed>
     can_ip_forward:                                      "false"
     cpu_platform:                                        <computed>
     create_timeout:                                      "4"
     deletion_protection:                                 "false"
     guest_accelerator.#:                                 <computed>
     instance_id:                                         <computed>
     label_fingerprint:                                   <computed>
     machine_type:                                        "f1-micro"
     metadata_fingerprint:                                <computed>
     metadata_startup_script:                             "sudo apt-get update; sudo apt-get install apache2 -y; echo Testando > /var/www/html/index.html"
     name:                                                "vm-webserver"
     network_interface.#:                                 "1"
     network_interface.0.access_config.#:                 "1"
     network_interface.0.access_config.0.assigned_nat_ip: <computed>
     network_interface.0.access_config.0.nat_ip:          <computed>
     network_interface.0.access_config.0.network_tier:    <computed>
     network_interface.0.address:                         <computed>
     network_interface.0.name:                            <computed>
     network_interface.0.network:                         "default"
     network_interface.0.network_ip:                      <computed>
     network_interface.0.subnetwork_project:              <computed>
     project:                                             <computed>
     scheduling.#:                                        <computed>
     self_link:                                           <computed>
     tags_fingerprint:                                    <computed>
     zone:                                                "northamerica-northeast1-a"


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.

Aparentemente tudo está conforme o esperado. O Terraform criará um recurso para nossa VM.

Antes de aplicarmos nosso plano de fato, volte à sua Dashboard do GCP e, no menu principal do canto esquerdo navegue até o painel de Instâncias (VMs):

Caso seja a sua primeira vez acessando esta seção, você deverá receber a informação de que o Google esta carregando a API de Compute Engine para seu uso. Este processo costuma levar cerca de 1 ou 2 minutos, mas apenas acontece na primeira vez que você acessa este painel. Para otimizar sua nuvem o Google não carrega todas as APIs por padrão, habilitando-as aos poucos conforme você as utiliza, bem como lhe permitindo habilitar apenas as que você deseja ou desabilitar as que não precisa.

Você provavelmente não terá nenhuma VM ou instância criada, portanto nada será listado para você além de uma tela informando que ali ficarão suas VMs, bem como um botão de Criar VMs, através do qual você poderia criar suas VMs e passar todas as suas definições pela interface. Mas o nosso objetivo é automatizar tudo isso e codificar nossa infraestrutura, certo?! Portanto, ignoremos isto por enquanto. O nosso objetivo nesta interface era apenas ver que não temos nossa vm webserver criada (ainda).

De volta ao nosso console ou terminal, executemos terraform apply:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
$ 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:

  + google_compute_instance.webserver
      id:                                                  <computed>
      boot_disk.#:                                         "1"
      boot_disk.0.auto_delete:                             "true"
      boot_disk.0.device_name:                             <computed>
      boot_disk.0.disk_encryption_key_sha256:              <computed>
      boot_disk.0.initialize_params.#:                     "1"
      boot_disk.0.initialize_params.0.image:               "debian-cloud/debian-9"
      boot_disk.0.initialize_params.0.size:                <computed>
      boot_disk.0.initialize_params.0.type:                <computed>
      can_ip_forward:                                      "false"
      cpu_platform:                                        <computed>
      create_timeout:                                      "4"
      deletion_protection:                                 "false"
      guest_accelerator.#:                                 <computed>
      instance_id:                                         <computed>
      label_fingerprint:                                   <computed>
      machine_type:                                        "f1-micro"
      metadata_fingerprint:                                <computed>
      metadata_startup_script:                             "sudo apt-get update; sudo apt-get install apache2 -y; echo Testando > /var/www/html/index.html"
      name:                                                "vm-webserver"
      network_interface.#:                                 "1"
      network_interface.0.access_config.#:                 "1"
      network_interface.0.access_config.0.assigned_nat_ip: <computed>
      network_interface.0.access_config.0.nat_ip:          <computed>
      network_interface.0.access_config.0.network_tier:    <computed>
      network_interface.0.address:                         <computed>
      network_interface.0.name:                            <computed>
      network_interface.0.network:                         "default"
      network_interface.0.network_ip:                      <computed>
      network_interface.0.subnetwork_project:              <computed>
      project:                                             <computed>
      scheduling.#:                                        <computed>
      self_link:                                           <computed>
      tags_fingerprint:                                    <computed>
      zone:                                                "northamerica-northeast1-a"


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:

Tudo parece correto, inclusive podemos ver nosso script de metadata que deverá ser executado durante a criação da VM para instalar nosso servidor WEB Apache. Confirme com um yes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Enter a value: yes

google_compute_instance.webserver: Creating...
boot_disk.#:                                         "" => "1"
boot_disk.0.auto_delete:                             "" => "true"
boot_disk.0.device_name:                             "" => "<computed>"
boot_disk.0.disk_encryption_key_sha256:              "" => "<computed>"
boot_disk.0.initialize_params.#:                     "" => "1"
boot_disk.0.initialize_params.0.image:               "" => "debian-cloud/debian-9"
boot_disk.0.initialize_params.0.size:                "" => "<computed>"
boot_disk.0.initialize_params.0.type:                "" => "<computed>"
can_ip_forward:                                      "" => "false"
cpu_platform:                                        "" => "<computed>"
create_timeout:                                      "" => "4"
deletion_protection:                                 "" => "false"
guest_accelerator.#:                                 "" => "<computed>"
instance_id:                                         "" => "<computed>"
label_fingerprint:                                   "" => "<computed>"
machine_type:                                        "" => "f1-micro"
metadata_fingerprint:                                "" => "<computed>"
metadata_startup_script:                             "" => "sudo apt-get update; sudo apt-get install apache2 -y; echo Testando > /var/www/html/index.html"
name:                                                "" => "vm-webserver"
network_interface.#:                                 "" => "1"
network_interface.0.access_config.#:                 "" => "1"
network_interface.0.access_config.0.assigned_nat_ip: "" => "<computed>"
network_interface.0.access_config.0.nat_ip:          "" => "<computed>"
network_interface.0.access_config.0.network_tier:    "" => "<computed>"
network_interface.0.address:                         "" => "<computed>"
network_interface.0.name:                            "" => "<computed>"
network_interface.0.network:                         "" => "default"
network_interface.0.network_ip:                      "" => "<computed>"
network_interface.0.subnetwork_project:              "" => "<computed>"
project:                                             "" => "<computed>"
scheduling.#:                                        "" => "<computed>"
self_link:                                           "" => "<computed>"
tags_fingerprint:                                    "" => "<computed>"
zone:                                                "" => "northamerica-northeast1-a"
google_compute_instance.webserver: Still creating... (10s elapsed)
google_compute_instance.webserver: Creation complete after 14s (ID: vm-webserver)

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

Nosso código parece ter sido executado com sucesso e o Terraform ao final nos confirma que um resource foi adicionado (o qual esperamos ser nossa VM).

Se atualizarmos nossa página de instâncias no GCP, devemos agora ver nossa vm-webserver criada e rodando:

Agora que temos a certeza de que nosso código funciona para a criação de nossa VM, precisamos também fazer com que a mesma seja acessível, afinal a principal função de qualquer servidor web é justamente ser acessível para apresentar alguma aplicação ou site. Para isto precisamos habilitar o firewall de nossa VM ou instância. Além disso, precisamos saber qual o endereço IP desta instância. Embora seja possível e simples conseguir este endereço IP através da interface do GCP ao clicarmos em cima de nossa VM, podemos também automatizar isto e receber este valor diretamente através do Terraform. (Lembre-se: O principal objetivo por trás da ideia de infraestrutura como código é automatizar ao máximo, evitando o uso de interfaces ou dashboards (odeio cliques) :p).

Vamos começar criando um arquivo outputs.tf que será utilizado para nos informar o IP da VM criada pelo Terraform:

outputs.tf
1
2
3
4
# Retorna o IP da VM criada
output "ip" {
  value = "${google_compute_instance.webserver.network_interface.0.access_config.0.nat_ip}"
}

Não, eu não estou inventando comandos ou trazendo algo do além. Se você é curioso e atencioso deve ter percebido que aqui estamos apenas puxando informações que o GCP criou através do Terraform. Se você voltar e reparar em seu comando terraform apply, perceberá que um dos atributos listados/criados em sua VM foi justamente o network_interface.0.access_config.0.nat_ip:

1
2
3
4
5
6
...
network_interface.0.access_config.0.assigned_nat_ip: "" => "<computed>"
--> network_interface.0.access_config.0.nat_ip:          "" => "<computed>"
network_interface.0.access_config.0.network_tier:    "" => "<computed>"
network_interface.0.address:                         "" => "<computed>"
...

Para validarmos que isto de fato funcionará, vamos executar novamente terraform apply:

1
2
3
4
5
6
7
8
$ terraform apply
google_compute_instance.webserver: Refreshing state... (ID: vm-webserver)

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

Outputs:

ip = 35.203.117.191

Conforme o esperado, nenhum resource precisou ser criado, pois não alteramos nada em nosso código indicando a necessidade de alguma mudança. A única coisa que aocnteceu de diferente foi que desta vez recebemos o IP de nossa instância ou VM.

Outra forma de recebermos este valor a qualquer momento é executando terraform output ip:

1
2
$ terraform output ip
35.203.117.191

Ótimo, já temos nossa instância e sabemos que podemos receber o endereço IP externo dela facilmente para acesso. Só nos resta agora abrir as portas de firewall necessárias para que possamos acessar nossa aplicação. Vamos abrir a porta 80 para nossa VM.

Vamos criar as seguintes variáveis em nosso arquivo variables.tf:

  • nome_fw: precisamos dar um nome para nosso firewall;
  • portas: precisamos criar uma lista de portas que serão abertas neste firewall.
variables.tf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
variable "project_id" {
  type    = "string"
  default = "possible-sun-83482736"
}

variable "regiao" {
  type = "string"
  default = "northamerica-northeast1"
}

variable "nome" {
  type = "string"
  default = "vm-webserver"
}

variable "tipo_maquina" {
  type = "string"
  default = "f1-micro"
}

variable "zona" {
  type = "string"
  default = "northamerica-northeast1-a"
}

variable "imagem" {
  type = "string"
  default = "debian-cloud/debian-9"
}

variable "nome_fw" {
  type = "string"
  default = "webserver-firewall"
}

variable "portas" {
  type = "list"
  default = ["80"]
}

A única novidade aqui foi o tipo list que utilizamos para a variável portas. Uma vez que podemos querer abrir múltiplas portas, utilizaremos o tipo list, o qual nos permite ter vários valores, ou uma lista de valores, para esta variável.

Agora vamos criar o resource para nosso firewall no arquivo main.tf:

main.tf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# Configura o projeto GCP
provider "google" {
  credentials = "${file("possible-sun-83482736-sabh45jhb2345ghv.json")}"
  project     = "${var.project_id}"
  region      = "${var.regiao}"
}

# Cria a VM com o Google Compute Engine
resource "google_compute_instance" "webserver" {
  name          = "${var.nome}"
  machine_type  = "${var.tipo_maquina}"
  zone          = "${var.zona}"

  boot_disk {
    initialize_params {
      image = "${var.imagem}"
    }
  }

  # Instala o servidor web Apache
  metadata_startup_script = "sudo apt-get update; sudo apt-get install apache2 -y; echo Testando > /var/www/html/index.html"

  # Habilita rede para a VM bem como um IP público
  network_interface {
    network = "default"
    access_config {

    }
  }
}

# Cria o Firewall para a VM
resource "google_compute_firewall" "webfirewall" {
  name        = "${var.nome_fw}"
  network     = "default"

  allow {
    protocol  = "tcp"
    ports     = "${var.portas}"
  }
}

Incluímos apenas um novo resource para a criação de nosso firewall abrindo as portas que definimos em nosso arquivo variables.tf com o protocolo tcp. Execute novamente terraform plan para ver o que mudaria:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ 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.

google_compute_instance.webserver: Refreshing state... (ID: vm-webserver)

------------------------------------------------------------------------

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:

  + google_compute_firewall.webfirewall
      id:                       <computed>
      allow.#:                  "1"
      allow.272637744.ports.#:  "1"
      allow.272637744.ports.0:  "80"
      allow.272637744.protocol: "tcp"
      creation_timestamp:       <computed>
      destination_ranges.#:     <computed>
      direction:                <computed>
      name:                     "webserver-firewall"
      network:                  "default"
      priority:                 "1000"
      project:                  <computed>
      self_link:                <computed>
      source_ranges.#:          <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 correto. Como a VM já existe e nada foi modificado no código da mesma, o Terraform apenas criará um novo resource para nosso firewall com os valores que definimos. Execute seu plano com terraform apply e confirme com yes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
$ terraform apply
google_compute_instance.webserver: Refreshing state... (ID: vm-webserver)

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:

  + google_compute_firewall.webfirewall
      id:                       <computed>
      allow.#:                  "1"
      allow.272637744.ports.#:  "1"
      allow.272637744.ports.0:  "80"
      allow.272637744.protocol: "tcp"
      creation_timestamp:       <computed>
      destination_ranges.#:     <computed>
      direction:                <computed>
      name:                     "webserver-firewall"
      network:                  "default"
      priority:                 "1000"
      project:                  <computed>
      self_link:                <computed>
      source_ranges.#:          <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

google_compute_firewall.webfirewall: Creating...
  allow.#:                  "" => "1"
  allow.272637744.ports.#:  "" => "1"
  allow.272637744.ports.0:  "" => "80"
  allow.272637744.protocol: "" => "tcp"
  creation_timestamp:       "" => "<computed>"
  destination_ranges.#:     "" => "<computed>"
  direction:                "" => "<computed>"
  name:                     "" => "webserver-firewall"
  network:                  "" => "default"
  priority:                 "" => "1000"
  project:                  "" => "<computed>"
  self_link:                "" => "<computed>"
  source_ranges.#:          "" => "<computed>"
google_compute_firewall.webfirewall: Still creating... (10s elapsed)
google_compute_firewall.webfirewall: Creation complete after 14s (ID: webserver-firewall)

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

Outputs:

ip = 35.203.117.191

Tudo parece ter funcionado. Um novo resource de firewall foi criado e, conforme visto anteriormente, também recebemos o nosso IP como output.

Confirme que tudo funcionou como o esperado copiando este IP e colando-o em seu navegador:

Se você recebeu uma página em branco com a palavra Testando, significa que tudo ocorreu conforme o esperado.

Até agora nossa infraestrutura possui:

  • 1 VM como servidor Web básico rodando Apache e uma página de teste;
  • 1 rede default;
  • 1 firewall básico aplicado à nossa VM abrindo a porta 80.

Embora esta seja uma infraestrutura extremamente simples, o Terraform lhe permite criar infraestruturas muito mais complexas e robustas.

No próximo post incrementaremos este código para incluírmos a criação de um cluster Kubernetes em nossa cloud.

Por hora, para não consumir muito de nossos créditos no GCP, vamos destruir nossa infraestrutura com terraform destroy. É tão simples e rápido criar tudo novamente agora que temos o código pronto, certo?! Então destruir tudo não nos causará problemas.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ terraform destroy
google_compute_firewall.webfirewall: Refreshing state... (ID: webserver-firewall)
google_compute_instance.webserver: Refreshing state... (ID: vm-webserver)

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:

  - google_compute_firewall.webfirewall

  - google_compute_instance.webserver


Plan: 0 to add, 0 to change, 2 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

google_compute_firewall.webfirewall: Destroying... (ID: webserver-firewall)
google_compute_instance.webserver: Destroying... (ID: vm-webserver)
google_compute_firewall.webfirewall: Still destroying... (ID: webserver-firewall, 10s elapsed)
google_compute_instance.webserver: Still destroying... (ID: vm-webserver, 10s elapsed)
google_compute_firewall.webfirewall: Destruction complete after 12s
google_compute_instance.webserver: Destruction complete after 14s

Destroy complete! Resources: 2 destroyed.

Tudo certo, sua infra foi completamente removida e seus créditos não mais serão utilizados agora.

Happy hacking!

Terraform: Variáveis E Outputs

| Comments

Onde paramos

Antes de seguir em frente com esta leitura gostaria de dizer que este post é continuação do anterior, onde dei uma breve introdução ao Terraform, com um exemplo prático em que fizemos o deployment de um jogo web do Mario em um container Docker.

Este post na verdade utilizará o mesmo código que escrevemos no post anterior, portanto se você não o leu, recomendo fortemente que o faça clicando aqui.

Variáveis

Embora nosso código tenha funcionado corretamente, ele não estava limpo. Existem algumas boas práticas que devemos sempre tentar seguir, Não apenas para deixar o código limpo, mas também para facilitar a manutenção do mesmo.

Imagine o seguinte código em Ruby:

1
2
3
4
5
puts "Meu nome é Marcelo."
puts "O Marcelo gosta de escrever códigos."
puts "Mas o Marcelo também gosta de surfar."
puts "Não sendo bom o suficiente a ponto de se tornar um profissional do surf, Marcelo decidiu seguir com a carreira de TI."
puts "Este é o Marcelo."

Um código extremamente simples que apenas imprime diversas strings na tela. Imagine que você precisa fazer manutenção deste código pois sua empresa agora decidiu que o personagem da história seria Pedro e não mais Marcelo. Claro, você pode ir lendo linha a linha e alterando em cada linha, mas isso leva muito mais tempo do que deveria. Imagine agora que este sistema possua algumas centenas de linhas de código. Ou múltiplos arquivos. Começa a ficar mais complexo e demorado alterar tudo, sem falar que fica fácil cometer o erro de esquecer algum. Por outro lado, se o nosso código utilizasse variáveis, apenas trocaríamos o valor em um local, tendo assim certeza absoluta de que o mesmo estaria correto em todo o código. Por exemplo:

1
2
3
4
5
6
7
nome = "Marcelo"

puts ("Meu nome é " + nome + ".")
puts ("O " + nome + " gosta de escrever códigos.")
puts ("Mas o " + nome + " também gosta de surfar.")
puts ("Não sendo bom o suficiente a ponto de se tornar um profissional do surf, " + nome + " decidiu seguir com a carreira de TI.")
puts ("Este é o " + nome + ".")

Neste código, quando precisarmos trocar o nome da pessoa e utilizar Pedro ao invés de Marcelo, precisaríamos alterar apenas o valor da variável na linha 1. muito mais simples, certo?!

Da mesma forma que em programação básica utilizamos variáveis, quando pensamos em infraestrutura como código deveríamos pensar da mesma forma, afinal estamos programando, certo?! Nao é um sistema, mas ainda assim estamos programando nossa infraestrutura.

Este é o nosso arquivo main.tf completo do post anterior:

main.tf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Baixar a imagem do Projeto Docker-SuperMario
resource "docker_image" "image_id" {
  name = "pengbai/docker-supermario:latest"
}

# Inicia o Container
resource "docker_container" "container_id" {
  name  = "supermario"
  image = "${docker_image.image_id.latest}"
  ports {
    internal = "8080"
    external = "80"
  }
}

# Nos informa o ip e nome do container criado
output "Endereco IP" {
  value = "${docker_container.container_id.ip_address}"
}

output "Nome do Container" {
  value = "${docker_container.container_id.name}"
}

Neste código não estamos utilizando variáveis, embora tenhamos um pouco de interpolação de valores. Vamos então começar a criar algumas variáveis, mas, seguindo as boas práticas do Terraform, criaremos um arquivo separado para nossas variáveis.

Crie um arquivo chamado variables.tf. O motivo pelo qual utilizaremos o nome em inglês aqui é por ser este o padrão adotado pelo Terraform. Ao chamarmos uma variável em nosso código, o Terraform saberá onde buscar o valor daquela variável.

Para cada variável daremos um nome, uma descrição e um valor default. Nosso arquivo variables.tf ficará assim:

variables.tf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
variable "nome_container" {
  description = "Nome do container"
  default = "supermario"
}

variable "imagem" {
  description = "Imagem do container"
  default = "pengbai/docker-supermario:latest"
}

variable "porta_interna" {
  description = "Porta interna do container"
  default = "8080"
}

variable "porta_externa" {
  description = "Porta externa do container"
  default = "80"
}

O que definimos:

  1. Criamos 4 variáveis aqui: nome_container, imagem, porta_interna e porta_externa;
  2. Para cada variável nós demos 2 atributos: description (descrição) e default (Valor padrão);
  3. Variáveis não precisam ser sempre declaradas. Existem ocasiões em que podemos criar uma variável sem qualquer valor atribuído à mesma, de forma que o valor será passado durante a execução do código, por exemplo. Por padrão, quando queremos que a variável possua um valor inicial padrão, o terraform utiliza o atributo default;
  4. A description, ou descrição, é um atributo também opcional, mas ajuda a identificar melhor o que se pretende com aquela variável e costuma ser uma boa prática, dando maior legibilidade ao seu código.

Agora que temos um arquivo com estas 4 variáveis, devemos voltar ao nosso arquivo main.tf e alterar um pouco nosso código para que possamos fazer uso destas variáveis. Iremso alterar nosso código bloco a bloco para ficar mais fácil identificarmos as diferenças.

Comecemos com o resource docker_image, que agora ficará da seguinte forma em nosso arquivo main.tf:

main.tf
1
2
3
4
# Baixar a imagem do Projeto Docker-SuperMario
resource "docker_image" "image_id" {
  name = "${var.imagem}"
}

O que alteramos:

  1. Nas linhas 1 e 2 não alteramos nada, pois são apenas comentários e a abertura de nosso resource;
  2. Na linha 3 tínhamos name = “pengbai/docker-supermario:latest” e agora temos name = “${var.imagem}”. Basicamente indicamos que o valor para name agora deverá ser pego a partir de nossa variável imagem em nosso arquivo variables.tf. Sim, para pegarmos o valor de uma variável, no Terraform, utilizamos sempre esta sintaxe: “${}”. Dentro das chaves iremos indicar onde se encontra a nossa variável. quando utilizamos o prefixo var, o Terraform busca automaticamente o valor em um arquivo variables.tf. Existem outras formas de declarar variáveis, mas não nos preocuparemos com isso por enquanto. Se checarmos novamente nosso arquivo variables.tf veremos a variável imagem que criamos, cujo valor é exatamente pengbai/docker-supermario:latest;
  3. Novamente, na linha 4, nenhuma alteração foi feita. Estamos apenas fechando nosso bloco de resource.

Vamos ao nosso próximo bloco de código, nosso resource docker_container. Alteremos o código para que fique da seguinte forma:

main.tf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Baixar a imagem do Projeto Docker-SuperMario
resource "docker_image" "image_id" {
  name = "${var.imagem}"
}

# Inicia o Container
resource "docker_container" "container_id" {
  name  = "${var.nome_container}"
  image = "${docker_image.image_id.latest}"
  ports {
    internal = "${var.porta_interna}"
    external = "${var.porta_externa}"
  }
}

Da mesma forma que fizemos antes, apenas trouxemos nossas variáveis:

  1. Na linha 8 passamos a utilizar a variável nome_container que criamos para dar o nome ao nosso container. Novamente, em nosso arquivo variables.tf você será capaz de encontrar a variável nome_container, cujo valor default é supermario;
  2. Na linha 11 apenas trocamos o valor 8080 pela variável porta_interna, assim como específicamos em nosso arquivo variables.tf;
  3. Na linha 12, assim como na linha 11, apenas trocamos o valor 80 pela variável porta_externa.

Nosso arquivo main.tf agora deverá estar assim:

main.tf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Baixar a imagem do Projeto Docker-SuperMario
resource "docker_image" "image_id" {
  name = "${var.imagem}"
}

# Inicia o Container
resource "docker_container" "container_id" {
  name  = "${var.nome_container}"
  image = "${docker_image.image_id.latest}"
  ports {
    internal = "${var.porta_interna}"
    external = "${var.porta_externa}"
  }
}

# Nos informa o ip e nome do container criado
output "Endereco IP" {
  value = "${docker_container.container_id.ip_address}"
}

output "Nome do Container" {
  value = "${docker_container.container_id.name}"

Acho sempre interessante fazer testes constantes em nosso código para ter certeza de que tudo está funcionando conforme o esperado. Como alteramos um pouco nosso código, criando variáveis em um arquivo variables.tf e removemos de nosso main.tf valores absolutos para fazermos uso de variáveis, é bom termos certeza de que não cometemos nenhum erro. Assumindo que temos o serviço do Docker rodando, vamos ao nosso terminal e, dentro de nosso diretório marioweb criado no posto anterior, vamos nos certificar de que destruímos a aplicação do post anterior para que não tenhamos nenhum container rodando:

1
$ terraform destroy

Agora vamos executar nosso plan:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
$ 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:

  + docker_container.container_id
      id:               <computed>
      attach:           "false"
      bridge:           <computed>
      container_logs:   <computed>
      exit_code:        <computed>
      gateway:          <computed>
      image:            "${docker_image.image_id.latest}"
      ip_address:       <computed>
      ip_prefix_length: <computed>
      log_driver:       "json-file"
      logs:             "false"
      must_run:         "true"
      name:             "supermario"
      network_data.#:   <computed>
      ports.#:          "1"
      ports.0.external: "80"
      ports.0.internal: "8080"
      ports.0.ip:       "0.0.0.0"
      ports.0.protocol: "tcp"
      restart:          "no"
      rm:               "false"
      start:            "true"

  + docker_image.image_id
      id:               <computed>
      latest:           <computed>
      name:             "pengbai/docker-supermario:latest"


Plan: 2 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.

Aparentemente está tudo correto. Na saída de nosso plan podemos ver que os valores estão de acordo com o esperado. Apliquemos então nosso código com terraform apply:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
$ 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:

  + docker_container.container_id
      id:               <computed>
      attach:           "false"
      bridge:           <computed>
      container_logs:   <computed>
      exit_code:        <computed>
      gateway:          <computed>
      image:            "${docker_image.image_id.latest}"
      ip_address:       <computed>
      ip_prefix_length: <computed>
      log_driver:       "json-file"
      logs:             "false"
      must_run:         "true"
      name:             "supermario"
      network_data.#:   <computed>
      ports.#:          "1"
      ports.0.external: "80"
      ports.0.internal: "8080"
      ports.0.ip:       "0.0.0.0"
      ports.0.protocol: "tcp"
      restart:          "no"
      rm:               "false"
      start:            "true"

  + docker_image.image_id
      id:               <computed>
      latest:           <computed>
      name:             "pengbai/docker-supermario:latest"


Plan: 2 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:

Assim como vimos no post anterior, o terraform apply sempre nos apresenta uma prévia das tarefas que serão executadas e em seguida nos pede uma confirmação de execução. Como tudo parece correto, vamos confirmar com um yes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
  Enter a value: yes

docker_image.image_id: Creating...
  latest: "" => "<computed>"
  name:   "" => "pengbai/docker-supermario:latest"
docker_image.image_id: Still creating... (10s elapsed)
docker_image.image_id: Still creating... (20s elapsed)
docker_image.image_id: Still creating... (30s elapsed)
docker_image.image_id: Creation complete after 37s (ID: sha256:49beaba1c5cc49d2fa424ac03a15b0e7...9c3d62pengbai/docker-supermario:latest)
docker_container.container_id: Creating...
  attach:           "" => "false"
  bridge:           "" => "<computed>"
  container_logs:   "" => "<computed>"
  exit_code:        "" => "<computed>"
  gateway:          "" => "<computed>"
  image:            "" => "sha256:49beaba1c5cc49d2fa424ac03a15b0e761f637e835c1ed4d8108cc247a9c3d62"
  ip_address:       "" => "<computed>"
  ip_prefix_length: "" => "<computed>"
  log_driver:       "" => "json-file"
  logs:             "" => "false"
  must_run:         "" => "true"
  name:             "" => "supermario"
  network_data.#:   "" => "<computed>"
  ports.#:          "" => "1"
  ports.0.external: "" => "80"
  ports.0.internal: "" => "8080"
  ports.0.ip:       "" => "0.0.0.0"
  ports.0.protocol: "" => "tcp"
  restart:          "" => "no"
  rm:               "" => "false"
  start:            "" => "true"
docker_container.container_id: Creation complete after 1s (ID: bbe9e8e7b5428532b882e7fbd304fc2b3d71e0bcb29fa099e15162397731e15e)

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

Outputs:

Endereco IP = 172.17.0.2
Nome do Container = supermario

Aparentemente tudo saiu conforme o esperado, com 2 resources adicionados, sendo eles nossa imagem e nosso container.

Novamente, podemos verificar que tudo está correto através do comando docker ps, onde deveremos ver que nosso container está rodando:

1
2
3
4
$ docker ps

CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS                  NAMES
bbe9e8e7b542        49beaba1c5cc        "catalina.sh run"   About a minute ago   Up About a minute   0.0.0.0:80->8080/tcp   supermario

Também podemos tentar acessar em nosso browser ou navegador o seguinte endereço: localhost:80. Nosso jogo do mario deverá estar funcionando.

Agora que entendemos o básico sobre o uso de variáveis e vimos que nosso código, embora um pouco diferente, continua funcionando, chegou a hora de corrigirmos nossos outputs. Eles continuam funcionando, porém para seguirmos os padrões e melhores práticas, vamos também retirá-los de nosso main.tf e criar um arquivo dedicado para isto.

Outputs

Comecemos criando um arquivo chamado outputs.tf com o seguinte conteúdo:

outputs.tf
1
2
3
4
5
6
7
8
# Nos informa o ip e nome do container criado
output "Endereco IP" {
  value = "${docker_container.container_id.ip_address}"
}

output "Nome do Container" {
  value = "${docker_container.container_id.name}"
}

Este foi fácil, certo?! Se prestarmos atenção, não alteramos praticamente nada. Apenas copiamos os dois blocos outputs do arquivo main.tf sem qualquer alteração.

Após salvar nosso arquivo outputs.tf, removeremos estes dois outputs do arquivo main.tf. Nosso main.tf ficará assim:

main.tf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Baixar a imagem do Projeto Docker-SuperMario
resource "docker_image" "image_id" {
  name = "${var.imagem}"
}

# Inicia o Container
resource "docker_container" "container_id" {
  name  = "${var.nome_container}"
  image = "${docker_image.image_id.latest}"
  ports {
    internal = "${var.porta_interna}"
    external = "${var.porta_externa}"
  }
}

Simples, não? Nosso código está mais limpo e organizado. Vamos destuir novamente nosso projeto com terraform destroy para que possamos testar estas últimas alterações:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ terraform destroy

docker_image.image_id: Refreshing state... (ID: sha256:49beaba1c5cc49d2fa424ac03a15b0e7...9c3d62pengbai/docker-supermario:latest)
docker_container.container_id: Refreshing state... (ID: bbe9e8e7b5428532b882e7fbd304fc2b3d71e0bcb29fa099e15162397731e15e)

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:

  - docker_container.container_id

  - docker_image.image_id


Plan: 0 to add, 0 to change, 2 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

docker_container.container_id: Destroying... (ID: bbe9e8e7b5428532b882e7fbd304fc2b3d71e0bcb29fa099e15162397731e15e)
docker_container.container_id: Destruction complete after 1s
docker_image.image_id: Destroying... (ID: sha256:49beaba1c5cc49d2fa424ac03a15b0e7...9c3d62pengbai/docker-supermario:latest)
docker_image.image_id: Destruction complete after 1s

Destroy complete! Resources: 2 destroyed.

Agora vamos aplicar nosso plan e em seguida, caso tudo esteja correto, vamos executar terraform plan:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
$ 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:

  + docker_container.container_id
      id:               <computed>
      attach:           "false"
      bridge:           <computed>
      container_logs:   <computed>
      exit_code:        <computed>
      gateway:          <computed>
      image:            "${docker_image.image_id.latest}"
      ip_address:       <computed>
      ip_prefix_length: <computed>
      log_driver:       "json-file"
      logs:             "false"
      must_run:         "true"
      name:             "supermario"
      network_data.#:   <computed>
      ports.#:          "1"
      ports.0.external: "80"
      ports.0.internal: "8080"
      ports.0.ip:       "0.0.0.0"
      ports.0.protocol: "tcp"
      restart:          "no"
      rm:               "false"
      start:            "true"

  + docker_image.image_id
      id:               <computed>
      latest:           <computed>
      name:             "pengbai/docker-supermario:latest"


Plan: 2 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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
$ 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:

  + docker_container.container_id
      id:               <computed>
      attach:           "false"
      bridge:           <computed>
      container_logs:   <computed>
      exit_code:        <computed>
      gateway:          <computed>
      image:            "${docker_image.image_id.latest}"
      ip_address:       <computed>
      ip_prefix_length: <computed>
      log_driver:       "json-file"
      logs:             "false"
      must_run:         "true"
      name:             "supermario"
      network_data.#:   <computed>
      ports.#:          "1"
      ports.0.external: "80"
      ports.0.internal: "8080"
      ports.0.ip:       "0.0.0.0"
      ports.0.protocol: "tcp"
      restart:          "no"
      rm:               "false"
      start:            "true"

  + docker_image.image_id
      id:               <computed>
      latest:           <computed>
      name:             "pengbai/docker-supermario:latest"


Plan: 2 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

docker_image.image_id: Creating...
  latest: "" => "<computed>"
  name:   "" => "pengbai/docker-supermario:latest"
docker_image.image_id: Still creating... (10s elapsed)
docker_image.image_id: Still creating... (20s elapsed)
docker_image.image_id: Still creating... (30s elapsed)
docker_image.image_id: Still creating... (40s elapsed)
docker_image.image_id: Creation complete after 42s (ID: sha256:49beaba1c5cc49d2fa424ac03a15b0e7...9c3d62pengbai/docker-supermario:latest)
docker_container.container_id: Creating...
  attach:           "" => "false"
  bridge:           "" => "<computed>"
  container_logs:   "" => "<computed>"
  exit_code:        "" => "<computed>"
  gateway:          "" => "<computed>"
  image:            "" => "sha256:49beaba1c5cc49d2fa424ac03a15b0e761f637e835c1ed4d8108cc247a9c3d62"
  ip_address:       "" => "<computed>"
  ip_prefix_length: "" => "<computed>"
  log_driver:       "" => "json-file"
  logs:             "" => "false"
  must_run:         "" => "true"
  name:             "" => "supermario"
  network_data.#:   "" => "<computed>"
  ports.#:          "" => "1"
  ports.0.external: "" => "80"
  ports.0.internal: "" => "8080"
  ports.0.ip:       "" => "0.0.0.0"
  ports.0.protocol: "" => "tcp"
  restart:          "" => "no"
  rm:               "" => "false"
  start:            "" => "true"
docker_container.container_id: Creation complete after 1s (ID: 5619b9c45b2509ca1a67cb1d43ea8e91f156f44245539604ad3dd060793900a4)

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

Outputs:

Endereco IP = 172.17.0.2
Nome do Container = supermario

Sucesso. Tudo saiu como o esperado e nossa aplicação está novamente no ar. Sinta-se livre para executar docker ps ou mesmo acessar localhost:80 em seu navegador para ter certeza de que tudo está funcionando e de que seu jogo Mario está no ar.

Atenção: Outputs como variáveis de saída

Da mesma forma que eu citei que existem outras formas de se declarar e utilizar variáveis, existem outras funções também para os outputs. Não, o Terraform não utiliza outputs apenas para apresentar informações na tela. A principal função dos outputs é na verdade a de variáveis de saída. Ou seja, pegar valores que poderão ser utilizados posteriormente. Este recurso é muito utilizado em projetos maiores com infraestruturas mais complexas mas, novamente, não é o foco deste post abordar isto.

Se você é um pouco atencioso e curioso, deve ter notado que não definimos os valores dos outputs em nenhum momento, certo? Por exemplo: value = “${docker_container.container_id.ip_address}”

Ou seja, estamos criando um output cujo valor será na verdade uma saída após o processamento de nosso código Terraform. Desta forma, nossos outputs são na verdade variáveis de saída.. mas isso já é uma outra história.

A propósito, se você além de curioso é também meticuloso, deve ter ficado confuso e questionado: Se outputs são na verdade uma espécie de variáveis, como podemos ter espaços em seus nomes? Como por exemplo “Nome do Container”?

A resposta é: Você me pegou. As melhores práticas pregam que não devemos criar outputs com espaços. Porque? Porque variáveis não podem conter espaços. Mas, como desde o início nosso objetivo era utilizar os outputs aqui apenas para nos retornar algum valor na tela, resolvi utilizar palavras em portugês e com espaços para facilitar a compreensão.

O ideal seria termos utilizado nome_do_container ao invés de Nome do Container, ou endereco_ip ao invés de Endereco IP mas, novamente.. isto é uma outra história.

Lembre-se de destuir o seu projeto para não deixar um container rodando desnecessariamente: $ terraform destroy

Em meu próximo post pretendo elevar um pouco o nível e utilizar o Terraform para criarmos uma infraestrutura básica na nuvem.

Happy Hacking!

Introdução Ao Terraform

| Comments

Terraform – Uma robusta opção para Infraestrutura como Código

A Hashicorp é uma empresa de bastante destaque no meio DevOps por ter criado várias soluções de automação, que englobam uma série de funcionalidades, como o Packer para criação de imagens de forma automatizada, conforme apresentado nestes dois posts do blog (1, 2), Vagrant, para provisionamento simples e rápido de máquinas, Vault, para gerenciamento de senhas/segredos (secrets), Consul, para descoberta de serviços, Nomad, para agendamento e automação de deployments, e o Terraform, foco principal deste post, uma robusta ferramenta para criação de infraestrutura como código, ou infrastructure as cdode.

Caso você não possua uma ideia muito clara de qual a idea por trás do conceito de infraestrutura como código, ou mesmo quais as vantagens de se utilizar esta metodologia de gerenciamento/criação de infraestrutura, sugiro que leia meu post anterior, no qual explico alguns dos principais benefícios desta prática, bem como uma breve apresentação do Terraform.

De forma resumida, o Terraform é uma ferramenta disponível em formatos Open Source ou Enterprise, cujo intuito é permitir a criação de infraestrutura como código, possibilitando o controle de versões. Suporta diversos provedores tais como AWS, OpenStack, Azure, GCP, etc.

Uma de suas principais características é a idempotência, termo muito utilizado na matemática ou em ciência da computação para indicar a propriedade que algumas operações têm de poderem ser aplicadas várias vezes sem que o valor do resultado se altere após a aplicação inicial. Ou seja, uma vez aplicado o seu código terraform, você poderá aplicá-lo quantas vezes desejar e nenhuma alteração será feita em sua infraestrutura, a menos que você tenha de fato alterado algo em seu código.

O Terraform utiliza uma linguagem de alto nível e fácil de se reutilizar, uma vez que podemos criar módulos e utilizar estes módulos em diversos projetos distintos, mesmo que tenhamos módulos em repositórios também distintos.

A ideia de possuir um “plano” de execução nos ajuda a identificar falhas em nosso código mais rapidamente, bem como prevenir problemas em nossa infraestrutura, visto que podemos ter uma visão geral de tudo o que será aplicado em nossa infra antes mesmo da execução real de nosso código, nos permitindo ter a certeza de que todas as alterações serão de fato intencionais.

Sempre digo que a melhor forma de se aprender uma nova tecnologia é colocando a mão na massa, portanto vamos escrever algumas linhas de código para entendermos as funcionalidades básicas bem como a sintaxe de código utilizada pelo Terraform.

Antes de pensarmos em cenários mais complexos devemos entender o básico, no entanto eu sempre gostei de ver algum resultado como forma de ter uma motivação real para meus estudos. Nunca gostei de apenas ler e escrever códigos que não resultam em nada, e acredito que todos devam sentir a mesma insatisfação ao não ter um uso real e prático para o que quer que esteja estudando.

Seguindo esta ideia, antes de pensarmos em cenários mais complexos, vamos iniciar pelo básico, porém com algum resultado prático. A ideia para este post é termos um jogo clássico do Mario rodando em um container Docker que seja acessível através de nosso browser.

Instalação

Para este post precisaremos ter três aplicativos instalados:

  1. Um navegador ou browser qualquer; (Imagino que você já tenha algum…)
  2. Docker;
  3. Terraform

Docker

O processo de instalação do Docker varia de acordo com o seu sistema operacional. Caso queira maiores detalhes sobre sua instalação, bem como uma explicação introdutória de como ele funciona, você pode visitar este outro post, embora você não precise ter nenhum conhecimento sobre Docker para seguir as instruções deste tutorial, visto que utilizaremos o Terraform para criar nosso container.

No Archlinux a instalação pode ser feita através do pacman:

1
# pacman install docker

No Windows a instalação pode ser feita através do binário disponível no site oficial: (https://store.docker.com/editions/community/docker-ce-desktop-windows)

No OS X, você também pode baixar o binário diretamente no site oficial, (https://store.docker.com/editions/community/docker-ce-desktop-mac) ou através do brew:

1
brew install docker

Terraform

A instalação do Terraform é tão simples quanto a do Docker.

No Archlinux a instalação pode ser feita através do pacman:

1
# pacman -S terraform

No Windows e no OS X a instalação pode ser feita através do binário disponível no site oficial do Terraform: (https://www.terraform.io/downloads.html)

Outra opção para OS X é através do brew:

1
brew install terraform

Verificando a instalação

Uma vez que você tenha instalado ambos, certifique-se de que a instalação foi bem sucedida e de que o serviço Docker esteja rodando em seu sistema. Para isto, abra algum terminal, console ou prompt do CMD (para usuários Windows) e digite o seguinte:

1- Para termos certeza de que o Terraform está instalado e funcionando:

1
terraform -version

Você deverá receber algum resultado com a versão do seu Terraform, similar a este:

1
Terraform v0.11.10

2- Para termos certeza de que o Docker está devidamente instalado e rodando, digite:

1
docker ps

Você deverá receber algum resultado parecido com o seguinte:

1
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

Não se assuste ou dê importância para este resultado do comando Docker, ele apenas indica o status atual de seu Docker, informando se você possui algum container rodando ou não. Caso você tenha acabado de instalar o mesmo ou iniciado o serviço do Docker, você provavelmente não terá nenhum container rodando.

Caso o retorno de seu Docker ou Terraform não seja similar aos que apresentei acima, verifique se a instalação foi realmente bem sucedida ou, no caso do Docker, verifique se o mesmo está rodando, afinal ele não apenas precisa ser instalado, mas precisa estar rodando, diferentemente do Terraform que apenas precisa ser instalado.

Iniciando nosso projeto

Escopo

O primeiro passo de qualquer projeto é identificar alguma espécie de esboço ou escopo para o mesmo.

O que queremos para o nosso projeto é:

  1. Ter um jogo web do Mario;
  2. Queremos que ele rode em um container, pois queremos uma aplicação em uma infraestrutura moderna e que possa ser capaz de rodar em qualquer local, seja em um servidor físico local, uma VM ou mesmo um provedor na nuvem, como AWS, GCP, Azure, etc.;
  3. Como não sabemos onde ou como iremos fazer o deployment deste container, queremos fazer com que seja algo automatizado e portável para facilitar futuros planos, portanto queremos criar este container com o Terraform, para que possamos gerenciar nosso código, versionar, etc.
  4. Não escreveremos a aplicação em si. O jogo do Mario já existe e uma imagem para Docker já está disponível para o mesmo através do seguinte link: (https://hub.docker.com/r/pengbai/docker-supermario/) (Mas nós não precisamos nos precoupar com isto agora, pois o Terraform vai cuidar de baixar a imagem para nós. ;])
  5. A aplicação deverá estar acessível via browser local para que possamos ao menos garantir que o jogo está de fato funcionando.

Este será o nosso escopo básico, portanto vamos começar nosso código.

Projeto

O recurso mais básico em um código ou módulo Terraform é o resource, ou recurso. O Terraform suporta centenas de recursos diferentes, dentre eles o docker_image, que será o recurso de que precisaremos inicialmente.

A partir deste momento não mais utilizarei a palavra recurso. Uma vez que o Terraform chama os recursos de resources, devemos nos acostumar com sua nomenclatura.

Antes de mais nada, vamos criar um diretório para nosso projeto. Chamaremos nosso projeto de Marioweb, visto que se trata de uma versao open source do jogo Mario.

Aqui estarei criando o diretório via linha de comando, mas sinta-se livre para criar um diretório da forma que você preferir. Após criar o diretório, com algum terminal ou console aberto (ou prompt do CMD para usuários do Windows), navegue até este diretório recém criado:

1
2
3
$ mkdir marioweb

$ cd marioweb

Dentro do diretório marioweb crie um novo arquivo chamado main.tf.

Para manter um padrão, os arquivos de código do Terraform costumam utilizar o sufixo/extensão .tf e o principal arquivo em módulos ou projetos Terraform costuma se chamar main.tf, por se tratar do arquivo principal do módulo ou projeto.

Para criar este arquivo você poderá utilizar qualquer editor de textos de sua escolha: vim, emacs, vi, notepad, notepad++, sublime, atom, etc.

Em nosso arquivo main.tf insira o seguinte conteúdo por enquanto:

main.tf

main.tf
1
2
3
4
# Baixar a imagem do Projeto Docker-SuperMario
resource "docker_image" "image_id" {
  name = "pengbai/docker-supermario:latest"
}

O que temos no bloco de código acima:

  1. A primeira linha é apenas um comentário. Como boa prática, é importante termos comentários ao longo de nosso código para descrever o que pretendemos com aquele determinado trecho de código. E por hora, o que pretendemos é exatamente apenas isso: Baixar a imagem do Projeto Docker-SuperMario para nosso ambiente.
  2. Na linha 2 estamos especificando que queremos utilizar um resource. Cada resource no Terraform leva dois parâmetros, sendo um deles o tipo de resource e o outro um nome qualquer para este resource. Como dito antes, este trecho de código pretende baixar a imagem do projeto SuperMario, portanto precisamos do tipo de resource chamado docker_image. Este é apenas um dos milhares de resources existentes para o Terraform. Em seguida estamos dando o nome image_id para nosso resource de tipo docker_image. O nome poderia ser qualquer um, até mesmo minha_imagem_do_coracao, mas para ficar mais descritiva e mantendo boas práticas, utilizarei image_id. Uma vez que identificamos o tipo de resource e o nome que queremos dar para ele, devemos encerrar a linha abrindo o bloco de código no qual listaremos os atributos deste resource. Para isto, utilizaremos um { para abrir este bloco.
  3. Na linha 3 começamos a definir os atributos do nosso resource. A documentação do Terraform é excelente e lista todos os resources suportados, bem como todos os atributos suportados por cada resource. Neste caso, o único atributo que precisamos no momento é o name, ou nome da imagem que desejamos baixar. Em seguida, indicamos qual o nome da imagem desejada. Por padrão, o Docker adota a nomenclatura <REPOSITÓRIO/IMAGEM:TAG> para indicar a imagem desejada. Em nosso caso, o repositório onde a imagem se encontra se chama pengbai e a imagem em si é chamada de docker-supermario, portanto teremos: name = “pengbai/super-mario”. A tag não é obrigatória. Mas como desejo garantir que utilizaremos sempre a imagem mais recente, utilizarei a tag latest (última).
  4. Uma vez que concluímos a definição de nosso resource, podemos fechar o nosso bloco de código para o mesmo utilizando um } na linha 4.

Agora que temos o início de nosso código, já podemos começar a testá-lo.

De volta ao nosso terminal/console, vamos iniciar o nosso ambiente Terraform para este projeto utilizando o comando terraform init. Este comando inicia nosso ambiente e baixa os plugins necessários para nosso projeto. No nosso caso, o Terraform baixará os plugins necessários para que nosso código possa lidar com o Docker.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ terraform init

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "docker" (1.1.0)...

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.docker: version = "~> 1.1"

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.

Se você recebeu um retorno parecido com o meu, significa que tudo está como deveria e que seu projeto foi iniciado com sucesso. Caso você liste os arquivos e diretórios ocultos de seu diretório, perceberá que ao rodar o comando terraform init, um diretório oculto chamado terraform foi criado. É nele que ficarão as informações que o Terraform precisa para executar corretamente o seu código, incluindo os plugins que ele necessita. No nosso caso, o plugin para o Docker estará lá.

Nosso próximo passo será executar o planejamento de nosso código. Ao rodar o planejamento o terraform listará exatamente tudo o que fará caso nosso código seja de fato executado. Novamente em nosso console/terminal, execute terraform plan:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ 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.

ATENÇÃO AQUI:
------------------------------------------------------------------------

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:

  + docker_image.image_id
      id:     <computed>
      latest: <computed>
      name:   "pengbai/docker-supermario:latest"


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.

Caso você tenha recebido um retorno similar a este, significa que tudo parece correto em seu código e que apenas uma ação será executada, conforme descrito no resumo do plano ao final:

Plan: 1 to add, 0 to change, 0 to destroy.

Ou seja: Plano: 1 a adicionar, 0 a alterar, 0 a destruir.

Exatamente o que queremos.

Caso você tenha recebido uma mensagem de erro, significa que algo em seu código está errado. Por exemplo, se ao invés de utilizarmos docker_image como tipo de resource, utilizarmos docker_images, o resultado de meu terraform plan seria o seguinte:

1
2
3
$ terraform plan

Error: docker_images.image_id: Provider doesn't support resource: docker_images

A mensagem geralmente é clara e nos indica onde está o erro. No caso acima, o Terraform nos diz que o resource docker_images não é suportado. Se checarmos a documentação do Terraform, veremos que o nome correto do resource é docker_image (no singular).

Uma vez que nosso plano foi executado sem erros, chegou a hora de aplicarmos nosso projeto.

Execute o seu código através do comando terraform apply:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ 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:

  + docker_image.image_id
      id:     <computed>
      latest: <computed>
      name:   "pengbai/docker-supermario:latest"


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:

Repare que mesmo ao utilizar apply ao invés de plan, uma espécie de planejamento também foi realizado antes da aplicação propriamente dita. O Terraform avaliou o código e nos indicou o que será realizado, perguntando-nos ao final se queremos ou não seguir com a execução. Caso tudo nos pareça correto, basta digitarmos yes e pressionar Enter novamente para que ele siga com a execução de fato.

1
2
3
4
5
6
7
8
9
10
11
12
  Enter a value: yes

docker_image.image_id: Creating...
  latest: "" => "<computed>"
  name:   "" => "pengbai/docker-supermario:latest"
docker_image.image_id: Still creating... (10s elapsed)
docker_image.image_id: Still creating... (20s elapsed)
docker_image.image_id: Still creating... (30s elapsed)
docker_image.image_id: Still creating... (40s elapsed)
docker_image.image_id: Creation complete after 47s (ID: sha256:49beaba1c5cc49d2fa424ac03a15b0e7...9c3d62pengbai/docker-supermario:latest)

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

Assim como no plan, o Terraform ao final nos deu um breve relatório do que foi feito:

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

Ou seja: Aplicação completa! Recursos: 1 adicionado, 0 alterados, 0 destruídos.

Se quisermos ter certeza de que de fato o Terraform baixou a imagem Docker de que precisamos, basta digitarmos o comando do Docker que lista as imagens que possuímos em nosso ambiente. A nossa nova imagem do supermario deverá estar lá. Digite docker images:

1
2
3
$ docker images
REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
pengbai/docker-supermario   latest              49beaba1c5cc        4 months ago        686MB

Ótimo, nossa imagem está presente em nosso ambiente.

O Terraform também nos permite saber o que estamos utilizando em termos de resources através do comando terraform show:

1
2
3
4
5
6
$ terraform show

docker_image.image_id:
  id = sha256:49beaba1c5cc49d2fa424ac03a15b0e761f637e835c1ed4d8108cc247a9c3d62pengbai/docker-supermario:latest
  latest = sha256:49beaba1c5cc49d2fa424ac03a15b0e761f637e835c1ed4d8108cc247a9c3d62
  name = pengbai/docker-supermario:latest

Voltemos ao nosso código. Agora que já conseguimos fazer com que nosso código baixe a imagem que utilizaremos via Docker, chegou a hora de fazer algo com ela. Precisamos realizar o deployment da mesma em um container, certo?!

Vamos adicionar mais um resource em nosso código, desta vez um resource de tipo docker_container. Como o nome já diz, este resource lida com o container em si, e não mais apenas com a imagem.

Seu código agora deverá estar da seguinte forma:

main.tf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Baixar a imagem do Projeto Docker-SuperMario
resource "docker_image" "image_id" {
  name = "pengbai/docker-supermario:latest"
}

# Inicia o Container
resource "docker_container" "container_id" {
  name  = "supermario"
  image = "${docker_image.image_id.latest}"
  ports {
    internal = "8080"
    external = "80"
  }
}

Ignorando as linhas já descritas anteriormente, vamos descrever as novas linhas de nosso código:

  1. Na linha 6 inserimos apenas mais um comentário, indicando que ali começaremos a descrever o código que criará nosso Container.
  2. Na linha 7 indicamos que queremos mais um resource. Desta vez o tipo de resource que queremos é o docker_container, indicando também que queremos dar o nome container_id a este resource. Novamente, ao fim da linha, abriremos o bloco de código para este resource com uma {.
  3. Dentro de nosso bloco, na linha 8, começaremos a listar os atributos deste resource. O primeiro atributo que listaremos é o name, e para ele daremos o nome supermario.
  4. Na linha 9 indicaremos o atributo image e utilizaremos nossa primeira interpolação, onde reutilizaremos valores de outra parte de nosso código como se fossem variáveis. Em nosso resource anterior, docker_image, demos um nome image_id que será utilizado agora. Incluiremos também a tag latest, pois, conforme pudemos ver na saída de nosso comando terraform show, esta foi a tag utilizada pelo terraform para identificar o último status daquele resource. Portanto, aqui utilizaremos a interpolação inserindo o que queremos entre {} seguidas do símbolo $, conforme prega a sintaxe do Terraform para interpolação de valores, ficando o seguinte: ${docker_image.image_id.latest}, onde docker_image é o tipo de resource de onde queremos o valor, image_id é o nome deste resource e latest é a tag para indicar que queremos o último valor daquele resource. O único motivo pelo qual temos um tipo de resource e um nome de resource é facilitar a identificação quando possuímos diversos resources do mesmo tipo. Imagine um projeto em que utilizaremos 5 imagens diferentes do Docker. Teríamos 5 resources do tipo docker_image, porém cada um deles teria um nome diferente, certo?!
  5. Na linha 10 de nosso código iniciamos o bloco de portas, afinal, toda aplicação roda em uma porta específica e com containers não seria diferente.
  6. Nas linhas 11 e 12 indicamos os valores para portas intern e externß, onde a porta intern será a porta utilizada pela aplicação internamente no container, e extern será a porta que o Docker irá mapear em nosso sistema local para que possamos acessar a nossa aplicação. Portanto, em nosso exemplo, a aplicação supermario irá rodar na porta 8080 internamente no container, e a porta 80 será mapeada para que possamos acessá-la de nosso navegador local.
  7. Nas linhas 13 e 14 apenas fecharemos os dois blocos de código que criamos, sendo estes o bloco ports e o bloco resource do docker_container.

Novamente, vamos planejar nosso projeto com terraform plan:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
$ 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.

docker_image.image_id: Refreshing state... (ID: sha256:49beaba1c5cc49d2fa424ac03a15b0e7...9c3d62pengbai/docker-supermario:latest)

------------------------------------------------------------------------

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:

  + docker_container.container_id
      id:               <computed>
      attach:           "false"
      bridge:           <computed>
      container_logs:   <computed>
      exit_code:        <computed>
      gateway:          <computed>
      image:            "sha256:49beaba1c5cc49d2fa424ac03a15b0e761f637e835c1ed4d8108cc247a9c3d62"
      ip_address:       <computed>
      ip_prefix_length: <computed>
      log_driver:       "json-file"
      logs:             "false"
      must_run:         "true"
      name:             "supermario"
      network_data.#:   <computed>
      ports.#:          "1"
      ports.0.external: "80"
      ports.0.internal: "8080"
      ports.0.ip:       "0.0.0.0"
      ports.0.protocol: "tcp"
      restart:          "no"
      rm:               "false"
      start:            "true"


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.

Repare que desta vez apenas 1 ação será executada: a criação do container. Não estamos mais recebendo as informações referentes à ação de baixar a imagem. O motivo para isto é a propriedade de idempotência que citei anteriormente. O Terraform sabe que a imagem já foi baixada, portanto a mesma não precisa ser baixada novamente, a menos que tivéssemos mudado a versão da mesma, nome, repositório, etc.

Uma vez que o plano esteja de acordo com o que queremos, podemos aplicar nosso código com terraform apply:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
$ terraform apply
docker_image.image_id: Refreshing state... (ID: sha256:49beaba1c5cc49d2fa424ac03a15b0e7...9c3d62pengbai/docker-supermario:latest)

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:

  + docker_container.container_id
      id:               <computed>
      attach:           "false"
      bridge:           <computed>
      container_logs:   <computed>
      exit_code:        <computed>
      gateway:          <computed>
      image:            "sha256:49beaba1c5cc49d2fa424ac03a15b0e761f637e835c1ed4d8108cc247a9c3d62"
      ip_address:       <computed>
      ip_prefix_length: <computed>
      log_driver:       "json-file"
      logs:             "false"
      must_run:         "true"
      name:             "supermario"
      network_data.#:   <computed>
      ports.#:          "1"
      ports.0.external: "80"
      ports.0.internal: "8080"
      ports.0.ip:       "0.0.0.0"
      ports.0.protocol: "tcp"
      restart:          "no"
      rm:               "false"
      start:            "true"


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:

Mais uma vez ele nos dá uma visão geral do que será feito e nos perguntará se queremos prosseguir. Digite yes e pressione Enter novamente.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Enter a value: yes

docker_container.container_id: Creating...
attach:           "" => "false"
bridge:           "" => "<computed>"
container_logs:   "" => "<computed>"
exit_code:        "" => "<computed>"
gateway:          "" => "<computed>"
image:            "" => "sha256:49beaba1c5cc49d2fa424ac03a15b0e761f637e835c1ed4d8108cc247a9c3d62"
ip_address:       "" => "<computed>"
ip_prefix_length: "" => "<computed>"
log_driver:       "" => "json-file"
logs:             "" => "false"
must_run:         "" => "true"
name:             "" => "supermario"
network_data.#:   "" => "<computed>"
ports.#:          "" => "1"
ports.0.external: "" => "80"
ports.0.internal: "" => "8080"
ports.0.ip:       "" => "0.0.0.0"
ports.0.protocol: "" => "tcp"
restart:          "" => "no"
rm:               "" => "false"
start:            "" => "true"
docker_container.container_id: Creation complete after 1s (ID: 8c9d35eac2fcdc0c7e530567323167b82e72f33d4645abf20685b99d802e2359)

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

Conforme o esperado: Aplicaçao completa! Resources: 1 adicionado, 0 alterados, 0 destruídos.

Mais uma vez podemos verificar se de fato tudo funcionou como o esperado através do Docker. Desta vez, não queremos apenas baixar uma imagem Docker, mas sim criar um container com a mesma abrindo portas específicas que serão mapeadas entre nosso sistema local e nosso container. Execute agora docker ps para ver os containers que estão rodando neste momento:

1
2
3
docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                  NAMES
8c9d35eac2fc        49beaba1c5cc        "catalina.sh run"   2 minutes ago       Up 2 minutes        0.0.0.0:80->8080/tcp   supermario

Como podemos ver, temos um container rodando. Podemos até ver que existe um mapeamento de portas: 80->8080

Não está convencido ainda?

Abra seu navegador e acesse o seguinte endereço: localhost:80

Caso o seu resultado seja algo parecido com a imagem, significa que seu código funcionou conforme o esperado.

O terraform também nos permite destruir a nossa infraestrutura com o comando terraform destroy. Da mesma forma que o apply, o comando destroy também lhe dará uma prévia de o que será destruído e lhe pedirá par aconfirmar com um yes ou no:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ terraform destroy
docker_image.image_id: Refreshing state... (ID: sha256:49beaba1c5cc49d2fa424ac03a15b0e7...9c3d62pengbai/docker-supermario:latest)
docker_container.container_id: Refreshing state... (ID: 8c9d35eac2fcdc0c7e530567323167b82e72f33d4645abf20685b99d802e2359)

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:

  - docker_container.container_id

  - docker_image.image_id


Plan: 0 to add, 0 to change, 2 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

docker_container.container_id: Destroying... (ID: 8c9d35eac2fcdc0c7e530567323167b82e72f33d4645abf20685b99d802e2359)
docker_container.container_id: Destruction complete after 0s
docker_image.image_id: Destroying... (ID: sha256:49beaba1c5cc49d2fa424ac03a15b0e7...9c3d62pengbai/docker-supermario:latest)
docker_image.image_id: Destruction complete after 2s

Destroy complete! Resources: 2 destroyed.

Como podemos ver, o terraform destruiu dois resources, nosso container e nossa imagem. Você poderá confirmar isto tentando acessar novamente o jogo pelo seu navegador ou mesmo através dos comandos docker images e docker ls para ver que tanto o container quanto a imagem foram removidos de nosso sistema:

1
2
3
4
5
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE

De volta ao nosso código, vamos incrementá-lo apenas um pouco mais.

O terraform nos permite especificar também outputs, ou saídas que nos serão apresentadas ao executarmos nosso código. Tratam-se de informações que podem nos ser úteis.

Por exemplo, supomos que ao executar nosso código, desejamos que o terraform nos informe o IP do container que foi criado e o nome do mesmo, nosso código agora ficaria assim:

main.tf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Baixar a imagem do Projeto Docker-SuperMario
resource "docker_image" "image_id" {
  name = "pengbai/docker-supermario:latest"
}

# Inicia o Container
resource "docker_container" "container_id" {
  name  = "supermario"
  image = "${docker_image.image_id.latest}"
  ports {
    internal = "8080"
    external = "80"
  }
}

# Nos informa o ip e nome do container criado
output "Endereco IP" {
  value = "${docker_container.container_id.ip_address}"
}

output "Nome do Container" {
  value = "${docker_container.container_id.name}"
}

Novamente, ignorando o código que já descrevemos anteriormente, teremos:

  1. Na linha 16 inserimos mais um comentário.
  2. Na linha 17 especificamos que desta vez queremos um output, e não mais um resource. Da mesma forma que fizemos com resources, vamos dar um nome a este output, de forma que possamos facilmente identificá-lo posteriormente. No caso, vamos chamar nosso output de Endereco IP. Em seguida abriremos o bloco de código para este output novamente com {.
  3. Na linha 18 daremos o value ou valor deste output. Novamente utilizaremos interpolação de valores para pegar este valor de nossos resources. Para conseguirmos o endereço ip do container, utilizaremos o atributo ip_address, que é um atributo descrito na documentação do Terraform como parte integrante do resource de tipo docker_container. Portanto, nosso value será: ${docker_container.container_id.ip_address}.
  4. Na linha 19, apenas fechamos o bloco deste output.
  5. Na linha 21 iniciamos nosso segundo output, com o nome Nome do Container. Em seguida, abriremos o bloco de código para este output com um {.
  6. Na linha 22, faremos algo similar ao que fizemos com o value do output anterior. Utilizaremos interpolação para buscar o valor do atributo name, que faz parte do resource docker_container. Nosso value será: ${docker_container.container_id.name}
  7. Na linha 23, apenas fechamos nosso output.

Vamos então rodar nosso plan para ver o que aconteceria desta vez:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
$ 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:

  + docker_container.container_id
      id:               <computed>
      attach:           "false"
      bridge:           <computed>
      container_logs:   <computed>
      exit_code:        <computed>
      gateway:          <computed>
      image:            "${docker_image.image_id.latest}"
      ip_address:       <computed>
      ip_prefix_length: <computed>
      log_driver:       "json-file"
      logs:             "false"
      must_run:         "true"
      name:             "supermario"
      network_data.#:   <computed>
      ports.#:          "1"
      ports.0.external: "80"
      ports.0.internal: "8080"
      ports.0.ip:       "0.0.0.0"
      ports.0.protocol: "tcp"
      restart:          "no"
      rm:               "false"
      start:            "true"

  + docker_image.image_id
      id:               <computed>
      latest:           <computed>
      name:             "pengbai/docker-supermario:latest"


Plan: 2 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.

Desta vez podemos ver que ambas as ações serão executadas: A imagem será baixada e o container será criado, afinal tínhamos removido tudo com terraform destroy anteriormente.

Vamos aplicar nosso código e confirmar com yes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
$ 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:

  + docker_container.container_id
      id:               <computed>
      attach:           "false"
      bridge:           <computed>
      container_logs:   <computed>
      exit_code:        <computed>
      gateway:          <computed>
      image:            "${docker_image.image_id.latest}"
      ip_address:       <computed>
      ip_prefix_length: <computed>
      log_driver:       "json-file"
      logs:             "false"
      must_run:         "true"
      name:             "supermario"
      network_data.#:   <computed>
      ports.#:          "1"
      ports.0.external: "80"
      ports.0.internal: "8080"
      ports.0.ip:       "0.0.0.0"
      ports.0.protocol: "tcp"
      restart:          "no"
      rm:               "false"
      start:            "true"

  + docker_image.image_id
      id:               <computed>
      latest:           <computed>
      name:             "pengbai/docker-supermario:latest"


Plan: 2 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

docker_image.image_id: Creating...
  latest: "" => "<computed>"
  name:   "" => "pengbai/docker-supermario:latest"
docker_image.image_id: Still creating... (10s elapsed)
docker_image.image_id: Still creating... (20s elapsed)
docker_image.image_id: Still creating... (30s elapsed)
docker_image.image_id: Still creating... (40s elapsed)
docker_image.image_id: Still creating... (50s elapsed)
docker_image.image_id: Creation complete after 59s (ID: sha256:49beaba1c5cc49d2fa424ac03a15b0e7...9c3d62pengbai/docker-supermario:latest)
docker_container.container_id: Creating...
  attach:           "" => "false"
  bridge:           "" => "<computed>"
  container_logs:   "" => "<computed>"
  exit_code:        "" => "<computed>"
  gateway:          "" => "<computed>"
  image:            "" => "sha256:49beaba1c5cc49d2fa424ac03a15b0e761f637e835c1ed4d8108cc247a9c3d62"
  ip_address:       "" => "<computed>"
  ip_prefix_length: "" => "<computed>"
  log_driver:       "" => "json-file"
  logs:             "" => "false"
  must_run:         "" => "true"
  name:             "" => "supermario"
  network_data.#:   "" => "<computed>"
  ports.#:          "" => "1"
  ports.0.external: "" => "80"
  ports.0.internal: "" => "8080"
  ports.0.ip:       "" => "0.0.0.0"
  ports.0.protocol: "" => "tcp"
  restart:          "" => "no"
  rm:               "" => "false"
  start:            "" => "true"
docker_container.container_id: Creation complete after 0s (ID: 655604d672af8ff76c10aca4cd169a6aa284dcca17f0e0215374fb18c86660fd)

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

Outputs:

Endereco IP = 172.17.0.2
Nome do Container = supermario

Repare que tudo o que queríamos foi executado e que, ao final, recebemos duas saídas ou outputs:

1
2
3
4
5
6
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

Endereco IP = 172.17.0.2
Nome do Container = supermario

Novamente, se você acessar em seu navegador o endereço localhost:80, ou utilizar os comandos docker ps, perceberá que sua aplicação está novamente rodando.

Não é complicado, certo?!

Obviamente, isto é apenas um exemplo extremamente simplista de uso do Terraform para criar uma pequena infraestrutura como código, o que em nosso caso é apenas um container.

No próximo post pretendo alterar um pouco este nosso código para utilizar algumas melhores práticas propostas pelo Terraform, como a utilização de variáveis e outputs em arquivos distintos, já que não utilizamos variáveis neste post.

Um passo de cada vez, certo?!

Happy Hacking!