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!

Infraestrutura Como Código Com Terraform

| Comments

Infraestrutura como Código (IAC – Infrastructure as Code)

Infraestrutura como código – IaC (ou infrastructure as code em inglês) – é o processo de gerenciamento e provisionamento de recursos de infraestrutura através de códigos ou arquivos de configuração que descrevem o estado desejado para tal infraestrutura ou recursos de infraestrutura. A principal característica de IaC é o uso de scripts ou definições declarativas ao invés de processos manuais, mas o termo é utilizado com mais frequência para promover abordagens declarativas. Como se tratam de arquivos de código, as definições podem ser armazenadas em um sistema de controle de versões, tal como o Git.

Abordagens IaC são comumente promovidas para computação em nuvem e, às vezes, são comercializadas como infraestrutura como serviço (infrastructure as a service, IaaS). IaC suporta IaaS, mas os dois conceitos não devem ser confundidos.

IaC e DevOps

IaC, ou Infraestrutura como Código, é um conceito bastante ligado à filosofia DevOps, visto que com práticas de implementação de uma infraestrutura baseada em códigos declarativos podemos aproximar as equipes de Operações e Desenvolvimento, fazendo com que os desenvolvedores tornem-se mais envolvidos nas configurações de máquinas ou recursos de infraestrutura como um todo, enquanto que os profissionais de Operações se envolvem mais cedo no processo de desenvolvimento. Além disso, agora ambas as equipes podem armazenar seu código em um mesmo ambiente, como por exemplo repositórios Git.

Infraestrutura como código mostrou-se uma excelente solução para livrar equipes de tarefas enfadonhas do cotidiano realizadas manualmente. Além de tomarem muito tempo e serem tarefas extremamente repetitivas, os corriqueiros processos manuais estão sujeitos a erros e podem colocar as operações em risco.

Algumas vantagens da utilização de IaC

  • Elimina tarefas repetitivas – Se você precisa criar 3 clusters Kubernetes em seu provedor de cloud (GCP ou AWS, por exemplo), você não precisa repetir os mesmos passos 3 vezes. Escreve um bloco de código que define a criação de um cluster e poderá aplicar este mesmo código quantas vezes forem necessárias;

  • Documentação simplificada – Não há necessidade de logar-se em um servidor ou provedor de cloud para tentar vasculhar tudo o que foi configurado (está tudo no código);

  • Reaproveitamento – Uma vez que tudo está codificado e separado em módulos, fica fácil reaproveitar módulos e código para futuras implementações;

  • Simples manutenção – Mudanças na configuração, versões, regras e demais definições podem ser implementadas e aplicadas rapidamente com pequenas alterações no código;

  • Versionamento – Ao abordar nossa infraestrutura como código passamos a ter diversos benefícios já rotineiros para desenvolvedores, como por exemplo a possibilidade de gerenciar nosso código em sistemas de versionamento como o Git, de forma a facilitar o trabalho em equipes, controle de versões, mudanças, etc;

  • Agilidade – Se preciso trocar a faixa de endereços IP de uma VPC ou subnet, alterar uma linha de código é muito mais rápido do que logar em uma dashboard, procurar tal recurso e alterar manualmente os valores desejados;

  • Possibilidade de soluções agnósticas – Em um mundo tecnológico que muda constantemente não são raras as ocasiões em que temos de mudar completamente nossa infraestrutura, seja deixando de usar servidores físicos para passar a utilizar VMs, ou migrando de VMs locais para a nuvem, ou de VMs na nuvem para containers, etc. Independente de qual seja o cenário de mudança, uma vez que sua infraestrutura está definida em código, dependendo das ferramentas escolhidas para codificar sua infra, o mesmo código poderia ser utilizado para um ambiente VMWare, AWS, Azure, GCP, etc, com poucas modificações. (Já citei agilidade e Simples manutenção, certo?!);

  • Fácil replicar – Em um ambiente não codificado ou automatizado, geralmente repetimos as mesmas configurações para criarmos ambientes distintos como Produção, Teste, Desenvolvimento, etc. Uma vez que sua infra está codificada, você aplica o mesmo código para criar quantos ambientes desejar;

  • Recuperação de Desastres ou Disaster Recovery – Desastres acontecem. Imagine um problema grande em sua infraestrutura. Em um cenário de virtualização, imagine que seu host VMWare simplesmente parou de funcionar pois seu disco queimou. Ou que o storage onde se encontravam as suas VMs simplesmente foi destruído. Ou, em um ambiente de cloud, imagine que sua senha de administrador da nuvem vazou e seu ambiente foi completamente excluído. Ou mesmo que o próprio data center ou região na qual se encontra a sua infra estrutura teve algum problema sério e toda a sua infra caiu. Claro, as boas práticas já pregam há muito tempo que sempre devemos ter backups de todos os servidores e sistemas, mas backups nada mais são do que arquivos. E a infraestrutura de fato? Você precisa ter uma infraestrutura ativa antes de conseguir restaurar backups, certo? Rede, firewall, VPCs, Clusters, Servidores, etc.. Se você possui toda a sua infraestrutura em código, recuperar tudo isso é tão simples quanto executar um único comando.

  • Planejamento (plan) e Testes – Práticas como planejamento e testes fazem parte (ou ao menos deveríam fazer) da rotina de praticamente qualquer desenvolvedor. Profissionais de infraestrutura sempre tiveram uma desvantagem em relação a isso, pois era complicado fazer testes de infraestrutura. Teste basicamente significava instalar exatamente a mesma infraestrutura em um ambiente isolado para testes. Ainda possível, porém pouco confiável e com altas chances de falhas, pois, por ser um processo manual e lento, não há qualquer garantia de que todos os mesmos passos serão executados no ambiente de produção tal como foram executados no ambiente de teste. Com infraestrutura como código, fica fácil utilizar-se das mesmas técnicas de planejamento e testes automatizados há muito utilizadas por desenvolvedores. Agora você consegue executar testes no código de sua infraestrutura que, literalmente, irão avaliar cada bloco de seu código e simular a execução de cada expressão ou descrição, dando-lhe assim uma visão geral sobre o que acontecerá, o que funcionará e o que falhará, garantindo uma integradade fiel entre teste e implantação em produção ou demais ambientes.

Lindo, não? Lembro de meus tempos de faculdade, quando costumava dizer a meus colegas que todos os profissionais de TI deveríam saber programar, independente de desejarem ou não trabalhar com programação. Na ocasião, em meados de 2006, a maioria deles dizia que isso era loucura. “Porque aprender a programar se vou trabalhar com infraestrutura?” Bom, tudo o que posso dizer hoje é: Bem vindos à era DevOps.

Quando falamos em infraestrutura como código, existem diversas ferramentas que trabalham em cima deste conceito, e muitas delas atuam juntas para englobar soluções mais completas, mas uma de minhas favoritas é o Terraform, da Hashicorp.

Escolha da ferramenta ideal para IAC

Com uma simples busca no Google pelo termo “ïnfraestrutura como código” ou “IAC”, será extremamente fácil encontrar diversas ferramentas, dentre as mais populares estão Chef, Puppet, Ansible, SaltStack, Terraform, CloudFormation, etc.

Um problema comum para quem inicia no mundo DevOps ou simplesmente deseja começar a utilizar infraestrutura como código é justamente a escolha. Qual a melhor ferramenta? Qual devo utilizar?

Eesta é uma dificuldade comum e inerante ao fato de se ter muitas opções. Se em uma sorveteria você só possui os sabores chocolate e baunilha, é extremamente simples optar por um, outro ou nenhum dos dois. No entanto, em uma sorveteria com 50 sabores, você provavelmente perderá alguns minutos apenas lendo todas as opções, do contrário irá apostar na sorte e escolher o primeiro que lhe parecer apetitoso, correndo o risco de descartar algum que não chegou a ver, mas que poderia ser muito melhor e refrescante para um dia quente como os do nordeste cearense.

O mais importante é sempre realizar uma pesquisa sobre pontos fortes e fracos de cada uma delas antes de tomar uma decisão, tendo em mente alguns aspectos:

  • A escolha não deve (ou não deveria) ser puramente pessoal. A melhor ferramenta dificilmente será a que você gostou mais de utilizar. A melhor ferramenta será a que melhor atende as necessidades do seu projeto ou negócio;

  • A análise deve ser feita em diversos aspectos, e não apenas em um ou dois. Supondo que você pesquise por exemplo quais possuem mais módulos gratuitos e quais delas possuem uma comunidade mais ativa na internet para dúvidas, mas esqueceu de ponderar o preço para ter acesso a suporte corporativo, você pode ter feito uma boa ou uma má escolha. Em caso de ser uma pequena empresa, com prazos relativamente longos para entregas de projetos e maior flexibilidade em termos de tempo fora do ar (downtime), o suporte corporativo pode não ser um fator decisivo. No entanto, em uma empresa de ambiente mais crítico, como um banco, demais sistemas financeiros, governamentais, etc., o suporte corporativo se torna um fator mais importante, portanto escolher uma ferramenta sem ponderar o valor de seu suporte corporativo acaba sendo um tiro no pé, o que reforça a ideia de que sempre devemos avaliar o máximo de aspectos possíveis e relevantes ao nosso projeto ou ambiente;

  • Outro fator fundamental e, em meu ver o mais importante, é entender que não necessariamente a escolha será exclusiva. Imagine que você precisa montar uma mesa que veio toda desmontada: tábuas, parafusos, gavetas, etc. Você tem algumas ferramentas a disposição, como chave de fendas, martelo, régua, serra, furadeira, etc. Você pode ser uma espécie de rambo e gostar de resolver as coisas com uma ferramenta só e, sinceramente, você pode até ser capaz de conseguir montar a mesa inteira apenas utilizando o martelo, parabéns por isso. Mas será que essa é a forma mais eficiente de resolver o problema? Será que não seria mais rápido e organizado utilizando um martelo e uma chave de fendas? Afinal, temos parafusos também, certo?!

Conforme descrito acima, o ideal é sempre avaliar o projeto ou ambiente no qual se irá trabalhar, sem a necessidade de escolher apenas uma ferramenta.

Sim, Chef, Puppet e Ansible são ferramentas de Infraestrutura como Código, no entanto elas possuem tarefas mais específicas, nas quais possuem mais desempenho, como por exemplo o gerenciamento de configurações, para não listar todas as suas funções.

E, claro, o CloudFormation é uma excelente ferramenta da Amazon para criação de infraestrutura como código, no entanto ela fica restrita ao ambiente de cloud da Amazon, o AWS. E se meu projeto estiver utilizando VMs em um ambiente VMWare? Ou se eu utilizar Google Cloud? Ou mesmo um ambiente mais heterogênio, com VMWare, Google Cloud e Amazon AWS? O CloudCloudFormation não seria a melhor opção, por ser restrito ao ambiente AWS.

Sempre fui a favor de utilizar as ferramentas corretas para cada tarefa em específico, portanto porque utilizar apenas uma se tenho outras disponíveis?

O Terraform, por outro lado, é específico para a criação da infraestrutura base, se saindo muito melhor do que Chef, Ansible ou Puppet nesta tarefa, mas nada impede (e eu encorajo e o faço) que você utilize Chef, Puppet ou Ansible, para gerenciar configurações, bootstraping ou deployments na infraestrutura criada pelo Terraform.

Além do mais, diferente do CloudFormation, o Terraform é uma solução agnóstica, permitindo-lhe criar infraestrutura em praticamente qualquer ambiente, seja ele em Cloud (Amazon AWS, Microsoft Azure, Google GCP, IBM Cloud, Digital Ocean, etc.), ambiente virtualizado, local ou em Data Centers (VMWare, Xen, Virtual Box, etc.), Docker, Kubernetes, além de recursos diversos de infraestrutura e softwares, tais como Redes (CloudFlare, DNS, DNSimple, F5 BIG-IP, Palo Alto Networks,etc.), Bancos de Dados (InfluxDB, MySQl ou PostgreSQL), dentre muitas outras coisas.

Terraform

O Terraform, da Hashicorp, lhe permite criar, alterar e melhorar sua infraestrutura de forma segura e previsível. É uma ferramenta Open Source que codifica APIs em arquivos de configuração declarativos que podem ser compartilhados entre membros de um time, tratados como código, editados, revisados e versionados.

A imagem acima descreve bem o fluxo básico da utilização de Terraform para codificar sua infraestrutura, na qual o fluxo mais simplista é:

  • Escrever o código de sua infraestrutura;
  • Planejar a execução do seu código, de forma que você receba informações antecipadamente de tudo o que acontecerá quando você aplicar o seu código;
  • Crie uma infraestrutura reproduzível ao aplicar seu código.

Apesar de este ser o fluxo mais simplista, com a utilização de infraestrutura como código você pode melhorar seu fluxo inserindo colaboração e compartilhamento, armazenando e gereciando seu código em um repositório git, por exemplo, além de ter assim um registro completo das mudanças e evoluções de sua infraestrutura, facilitando a automação em fluxos mais complexos, como por exemplo em pipelines de Integração Contínua.

O Terraform funciona basicamente através de recursos, ou resources, que definem o tipo de infraestrutura você estará criando bem como seus atributos. Além disso, conforme dito anteriormente, o Terraform também pode ser utilizado em paralelo com diversas outras ferramentas de automação em forma de provedores, ou providers, como Puppet, Chef, Ansible, etc.

Por estarmos tentando aplicar para a infraestrutura um conceito que seja mais próximo do que já era utilizado por desenvolvedores há muito tempo, o Terraform possui também uma abordagem que lhe permite reaproveitamento de código, através de módulos. Existem diversos módulos criados e disponibilizados gratuitamente, mas você pode também criar seus próprios módulos de forma a melhor organizar e reaproveitar seu próprio código em diversos projetos.

Arquivos de configuração descrevem ao Terraform os componentes necessários para rodar uma única aplicação que representa todo o seu datacenter. O Terraform gera um plano de execução descrevendo o que fará para alcançar o estado desejado, e em seguida, caso aprovado, o executará para criar a infraestrutura desejada. Conforme a configuração muda, o Terraform será capaz de determinar o que mudou e criará planos de execução incrementais que podem ser aplicados.

Vejamos o seguinte diagrama que descreve uma simples infraesturura. Imaginemos que esta é a infraestrutura que queremos rodar em nossa conta no Google Cloud para termos um site de e-commerce:

O diagrama acima possui diversos elementos:

  1. Uma organização no Google Cloud chamada Kalib Avante;
  2. Um diretório ou Folder (como chamado no Google Cloud) chamado Projetos;
  3. Um projeto chamado E-Commerce;
  4. Um projeto chamado Zebra Feliz;
  5. Dentro do projeto E-Commerce temos dois ambientes: Produção e Teste
  6. Repare que o projeto Zebra Feliz está incompleto e sem ambientes distintos, como Produção, teste, etc. Bom, trata-se de um projeto piloto ainda em desenvolvimento e planejamento, portanto os recursos não foram ainda criados por completo. Mas o Terraform nos permite incrementar recursos quando necessário, certo? Portanto, sem problemas com isto por enquanto.

Esta é a estrutura básica, já em termos de recursos temos Firewalls, clusters kubernetes com Nodes, buckets de storage para conservar o status ou state do Terraform e por consequência de sua infraestrutura, Discos persistentes, Container Registry (repositório de imagens Docker), VPCs, Load Balancer, VMs, etc.

Caso esteja se perguntando, sim o Terraform lhe permite criar esta infraestrutura inteira, bem como outras bem mais complexas, com mais projetos, mais ambientes, mais recursos, etc.

Desta forma podemos ter um código terraform dividido em alguns módulos, armazenado em um repositório Git, por exemplo, e criar toda essa infraestrutura, desde a Organização vazia, ao diretório de projetos, aos 2 projetos em si, buckets, clusters kubernetes, DNS, IAM, load balancer, VMs por trás do Load Balancer, etc. Tudo isto com um único comando:

1
terraform apply

Lembrando um pouco do que falamos lá em cima, sobre ser simples reproduzir, ou se recuperar de desastres… imagine que uma região inteira caiu no Google, onde temos nossa infraestrutura. Sim, eu sei que isso é extremamente raro, mas vamos imaginar os cenários mais absurdos e raros também. Imagine que perdi toda a minha infraestrutura. Imagine ter que recriar tudo isso (projetos, diretórios, storages, DNS, IAM, clusters, Load Balancer, VMs, etc, etc..) manualmente..? Demoraria bastante, certo?! Mas, como fomos espertos e criamos tudo via terraform, um simples terraform apply irá criar tudo novamente para nós, exatamente como era antes.

O mesmo se dá caso precisemos recriar toda essa mesma infraestrutura em uma nova região do Google, ou caso queiramos destruir nossa infra e recriá-la em outra zona, por algum motivo.

Aqui estamos lidando apenas com a Infraestrutura pois, conforme dito antes, o Terraform é excelente para criar a infraestrutura, mas o ideal ainda é utilizar outros softwares para provisionamento e deployment. Por exemplo, uma vez que temos nossa infraestrutura inteira criada, podemos começar a provisionar os sistemas e softwares através de outras ferrmentas, como Helm (para deployment dentro dos clusters Kubernetes), Chef, Puppet, Ansible, etc.

A ideia deste post é apenas dar uma introdução teórica, uma ideia de como infraestrutura como código funciona, e de como o Terraform é capaz de fazer tudo isso de forma segura e robusta. (Embora eu já veja uma enorme barra de rolagem aqui ao lado e sei que lhe fiz ler bastante, supondo que leu até aqui. :p)

Em meu próximo post pretendo fazer uma abordagem mais prática e com mão na massa, utilizando de fato o Terraform para criar uma simples infraestrutura via código.

Happy Hacking!