Post

Provisionamento de Infraestrutura KVM com Terraform e Módulos Reutilizáveis

Provisionamento de Infraestrutura KVM com Terraform e Módulos Reutilizáveis

Introdução

No cenário atual de infraestrutura como código (IaC), a automação é fundamental para garantir agilidade, consistência e escalabilidade. Este tutorial foi desenvolvido para administradores de sistemas, engenheiros de DevOps e entusiastas de virtualização que desejam automatizar o provisionamento de máquinas virtuais KVM utilizando o Terraform. Abordaremos desde a configuração inicial do ambiente até a implantação e gerenciamento de VMs, com ênfase na criação de módulos Terraform reutilizáveis para otimizar seu fluxo de trabalho.

Ao final deste guia, você será capaz de:

  • Configurar seu ambiente com KVM e Terraform.
  • Compreender a estrutura de um projeto Terraform modular para KVM.
  • Automatizar a criação de redes e máquinas virtuais.
  • Utilizar o Cloud-Init para personalização inicial das VMs.

Estrutura do Projeto

O projeto Terraform para provisionamento de VMs KVM é organizado de forma modular para promover a reusabilidade e a clareza. Abaixo, detalhamos a estrutura de diretórios e a finalidade de cada arquivo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.
├── .gitignore            # Regras para ignorar arquivos sensíveis e temporários, garantindo que credenciais e estados do Terraform não sejam versionados.
├── main.tf               # O arquivo de configuração principal do Terraform. Ele orquestra a criação dos recursos, chamando módulos e definindo a lógica de alto nível da infraestrutura.
├── modules               # Diretório que contém os módulos Terraform reutilizáveis. Cada subdiretório aqui representa um módulo independente.
│   └── vm                # **Módulo VM**: Este módulo é responsável por encapsular toda a lógica e os recursos necessários para provisionar uma única máquina virtual KVM, incluindo discos, cloud-init e a definição do domínio KVM.
│       ├── cloud-init    # Contém os templates do Cloud-Init (`.tpl`) que serão usados para personalizar as VMs no primeiro boot, configurando usuários, SSH e rede.
│       │   ├── network_config.yml.tpl # Template Cloud-Init para configuração de rede da VM.
│       │   └── user_data.yml.tpl    # Template Cloud-Init para configuração de usuário, chaves SSH e comandos de inicialização da VM.
│       ├── main.tf       # Define os recursos Terraform específicos para o módulo VM (ex: `libvirt_cloudinit_disk`, `libvirt_volume`, `libvirt_domain`).
│       ├── outputs.tf    # Declara os valores de saída do módulo VM (ex: IPs das VMs, nomes de domínio) que podem ser referenciados pelo módulo pai ou pelo usuário.
│       ├── variables.tf  # Define as variáveis de entrada para o módulo VM, permitindo que ele seja configurado de forma flexível.
│       └── versions.tf   # Especifica as versões mínimas e máximas dos provedores Terraform que este módulo requer, garantindo compatibilidade.
├── network.tf            # Define os recursos de rede Libvirt, como a rede virtual que as VMs utilizarão para comunicação.
├── provider.tf           # Configura os provedores Terraform necessários para o projeto (neste caso, o provedor `libvirt`).
├── terraform.tfvars      # Arquivo para armazenar valores de variáveis sensíveis ou específicos do ambiente (ex: chaves SSH, configurações de VM). **Este arquivo não deve ser versionado!**
├── terraform.tfvars.example # Um exemplo do arquivo `terraform.tfvars`, mostrando a estrutura e os tipos de valores esperados, sem conter dados sensíveis.
└── variables.tf          # Declara as variáveis globais do projeto, que podem ser usadas em `main.tf`, `network.tf` e passadas para os módulos.

Entendendo os Módulos Terraform:

Os módulos Terraform são contêineres para múltiplas configurações de recursos que são usadas em conjunto. Eles permitem que você organize seu código, encapsule a complexidade e crie componentes reutilizáveis. No nosso caso, o módulo vm abstrai a criação de uma máquina virtual KVM, permitindo que o main.tf simplesmente “chame” esse módulo várias vezes para provisionar múltiplas VMs com configurações diferentes, sem duplicar código.

1. Configuração dos Arquivos Terraform

Nesta seção, exploraremos os principais arquivos de configuração do Terraform que compõem nosso projeto. Cada arquivo desempenha um papel crucial na definição e orquestração da nossa infraestrutura KVM.

1.1. provider.tf

O arquivo provider.tf é onde configuramos os provedores Terraform que serão utilizados em nosso projeto. Provedores são plugins que o Terraform usa para interagir com APIs de diferentes serviços de infraestrutura (neste caso, o libvirt para KVM). É crucial definir o provedor corretamente para que o Terraform possa se comunicar com o ambiente KVM.

1
2
3
4
5
6
7
8
9
10
11
12
terraform {
  required_providers {
    libvirt = {
      source  = "dmacvicar/libvirt" # Define a origem do provedor, indicando onde o Terraform deve baixá-lo.
      version = "0.8.3"             # Especifica a versão exata do provedor a ser utilizada, garantindo compatibilidade e reprodutibilidade.
    }
  }
}

provider "libvirt" {
  uri = var.libvirt_uri # Define o URI de conexão para o daemon libvirtd. Este valor é uma variável para flexibilidade.
}

Explicação Detalhada:

  • terraform { required_providers { ... } }: Este bloco é fundamental para o Terraform. Ele declara quais provedores são necessários para o projeto e de onde eles devem ser obtidos. Ao especificar a source e a version, garantimos que o Terraform baixe e utilize a versão correta do provedor libvirt, evitando problemas de compatibilidade.
  • provider "libvirt" { ... }: Este bloco configura o provedor libvirt em si. A propriedade uri é essencial, pois informa ao provedor como se conectar ao seu ambiente KVM. Utilizamos var.libvirt_uri para tornar este valor configurável através de variáveis, permitindo que você altere o URI de conexão sem modificar o código do provedor. O valor padrão para libvirt_uri será definido em variables.tf.

1.2. variables.tf

O arquivo variables.tf é onde declaramos as variáveis de entrada para o nosso projeto Terraform. Variáveis permitem que você personalize o comportamento do seu código Terraform sem precisar modificar o código diretamente, tornando-o mais flexível e reutilizável. É uma prática recomendada para desacoplar a configuração do código, facilitando a gestão de diferentes ambientes (desenvolvimento, produção, etc.).

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
variable "libvirt_uri" {
  type        = string
  default     = "qemu:///system" # Valor padrão para a URI de conexão, útil para ambientes de desenvolvimento local.
  description = "URI de conexão para o daemon libvirtd. O padrão 'qemu:///system' conecta-se ao sistema KVM local."
}

variable "network_name" {
  type        = string
  default     = "tfnet" # Nome padrão para a rede virtual, pode ser sobrescrito.
  description = "Nome da rede virtual Libvirt a ser criada."
}

variable "network_mode" {
  type        = string
  default     = "nat" # Modo NAT é comum para VMs que precisam de acesso à internet mas não de IPs públicos.
  description = "Modo de operação da rede Libvirt (ex: 'nat' para NAT, 'bridge' para bridge)."
}

variable "network_addresses" {
  type        = string
  default     = "10.64.0.0/24" # Define o bloco de IPs para a rede virtual.
  description = "Endereço IP e máscara de sub-rede para a rede virtual Libvirt."
}

variable "ssh_public_key" {
  description = "Conteúdo da chave SSH pública para acesso às VMs. É recomendado que este valor seja fornecido via terraform.tfvars ou variáveis de ambiente por ser sensível." # Importante para segurança, não deve ser hardcoded.
  type        = string
  sensitive   = true # Marca a variável como sensível, ocultando seu valor em logs e saídas do Terraform.
}

variable "vms" {
  type = map(object({
    username      = string
    gecos         = string
    groups        = list(string)
    network_ip    = string
    nameserver_ip = string
    route_ip      = string
    memory        = number
    vcpu          = number
    os_image_url  = string
  }))
  sensitive = false # Este mapa contém configurações, mas não dados sensíveis diretamente.
  description = "Um mapa de configurações para cada máquina virtual a ser provisionada. A chave do mapa é o hostname da VM." # Permite a criação de múltiplas VMs com configurações distintas de forma dinâmica.

  validation {
    condition     = length(var.vms) == length(distinct(keys(var.vms))) # Garante que todos os hostnames sejam únicos para evitar conflitos.
    error_message = "Todos os hostnames devem ser únicos."
  }

  validation {
    condition     = alltrue([for k, v in var.vms : can(regex("^[a-z0-9-]{1,63}$", k))]) # Valida o formato do hostname de acordo com padrões de DNS.
    error_message = "Hostname deve conter apenas letras minúsculas, números e hifens, com até 63 caracteres."
  }

  validation {
    condition = alltrue([
      for k, v in var.vms :
      can(regex("^\\d+\\.\\d+\\.\\d+\\.\\d+$", v.network_ip))
    ])
    error_message = "Endereços IP devem estar no formato IPv4 válido." # Assegura que os IPs fornecidos são válidos.
  }

  validation {
    condition = alltrue([
      for k, v in var.vms :
      v.memory >= 512 && v.memory <= 65536
    ])
    error_message = "Memória deve estar entre 512MB e 64GB." # Define um range aceitável para a memória da VM.
  }
}

Explicação Detalhada:

Cada bloco variable define uma variável de entrada. As propriedades comuns incluem:

  • description: Uma descrição clara e concisa do propósito da variável, essencial para a documentação do seu código Terraform.
  • type: O tipo de dado esperado para a variável (ex: string, number, list, map, object). Definir o tipo ajuda o Terraform a validar a entrada e evita erros inesperados.
  • default: Um valor padrão para a variável, caso não seja fornecido explicitamente. Isso torna as variáveis opcionais e simplifica o uso para configurações comuns.
  • sensitive: Se true, o valor da variável será ocultado na saída do Terraform para proteger informações sensíveis (como chaves SSH, senhas, etc.). Isso é uma medida de segurança crucial.
  • validation: Blocos opcionais para definir regras de validação para os valores das variáveis. As validações garantem que os dados fornecidos estejam no formato ou intervalo esperado, prevenindo erros antes mesmo do provisionamento da infraestrutura. Por exemplo, validamos o formato de IPs e o range de memória para as VMs.

1.3. main.tf

O arquivo main.tf é o ponto de entrada principal do nosso projeto Terraform. Ele é responsável por orquestrar a criação dos recursos, chamando os módulos definidos e conectando as diferentes partes da nossa infraestrutura. É aqui que a modularidade do nosso projeto se torna evidente, pois em vez de definir cada VM individualmente, nós simplesmente invocamos o módulo vm e passamos as configurações necessárias.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module "vm_cluster" {
  source = "./modules/vm"

  # Passa o mapa de configurações de VMs (definido em variables.tf) para o módulo 'vm'.
  # O módulo irá iterar sobre este mapa para criar múltiplas VMs.
  vm_configs     = var.vms

  # Conecta as VMs à rede virtual Libvirt criada em network.tf.
  # O 'id' é uma saída do recurso 'libvirt_network.tf_network'.
  network_id     = libvirt_network.tf_network.id

  # Passa a chave SSH pública para o módulo 'vm'.
  # Esta chave será usada pelo Cloud-Init para configurar o acesso SSH às VMs.
  ssh_public_key = var.ssh_public_key
}

Explicação Detalhada:

  • module "vm_cluster" { ... }: Este bloco declara uma instância do módulo Terraform. O nome vm_cluster é um identificador lógico que você atribui a esta invocação específica do módulo. Isso é útil quando você precisa referenciar as saídas ou o estado deste conjunto de VMs.
  • source = "./modules/vm": Este argumento crucial informa ao Terraform onde encontrar o código do módulo. No nosso caso, "./modules/vm" aponta para um diretório local chamado vm dentro da pasta modules. Isso significa que estamos usando um módulo local, o que é ideal para encapsular a lógica específica do nosso projeto.
  • vm_configs = var.vms: Esta linha demonstra como as variáveis são passadas para um módulo. var.vms refere-se à variável vms declarada no arquivo variables.tf do projeto raiz. O Terraform pega o valor dessa variável (que é um mapa de configurações para cada VM) e o atribui à variável de entrada vm_configs dentro do módulo vm. O módulo vm então usa este mapa para criar dinamicamente várias máquinas virtuais.
  • network_id = libvirt_network.tf_network.id: Aqui, estamos passando o ID da rede virtual Libvirt que foi criada no arquivo network.tf. libvirt_network.tf_network.id é uma referência a um atributo de saída do recurso libvirt_network nomeado tf_network. Isso garante que todas as VMs criadas por este módulo sejam anexadas à rede correta.
  • ssh_public_key = var.ssh_public_key: Similar a vm_configs, esta linha passa o valor da variável ssh_public_key (do variables.tf raiz) para o módulo vm. O módulo vm utilizará esta chave para configurar o acesso SSH às VMs através do Cloud-Init, permitindo que você se conecte a elas após o provisionamento.

1.4. network.tf

O arquivo network.tf é dedicado à definição da rede virtual Libvirt que será utilizada pelas máquinas virtuais. Uma rede virtual é essencial para permitir a comunicação entre as VMs e, dependendo da configuração, com a rede externa. A configuração da rede é um passo crucial para garantir que suas VMs possam se comunicar entre si e com o mundo exterior, se necessário.

1
2
3
4
5
resource "libvirt_network" "tf_network" {
  name      = var.network_name    # Define o nome da rede virtual, obtido da variável global `network_name`.
  mode      = var.network_mode    # Especifica o modo de operação da rede (e.g., 'nat', 'bridge'), também configurável via variável.
  addresses = [var.network_addresses] # Define o bloco de endereços IP para a rede, permitindo a personalização da sub-rede.
}

Explicação Detalhada:

  • resource "libvirt_network" "tf_network" { ... }: Este bloco declara um recurso do tipo libvirt_network, que representa uma rede virtual gerenciada pelo Libvirt. O nome tf_network é um identificador lógico que o Terraform usa para referenciar esta rede dentro do seu código.
  • name = var.network_name: Atribui um nome à rede virtual. Este nome é importante para identificar a rede no Libvirt e pode ser configurado através da variável network_name (definida em variables.tf), que por padrão é tfnet.
  • mode = var.network_mode: Define o modo de operação da rede. O modo nat (Network Address Translation) é o mais comum para ambientes de teste e desenvolvimento, pois permite que as VMs acessem a internet usando o IP do host, enquanto as isola da rede física principal. Outras opções incluem bridge para integração direta com a rede física.
  • addresses = [var.network_addresses]: Especifica o bloco de endereços IP que será usado pela rede virtual. O valor é obtido da variável network_addresses (por padrão 10.64.0.0/24), que define a sub-rede e a máscara. O Terraform usará essa informação para configurar o servidor DHCP e o roteamento para as VMs conectadas a esta rede.### 1.5. modules/vm/main.tf

O arquivo main.tf dentro do módulo vm é o coração da definição da máquina virtual. Ele contém os recursos Terraform que efetivamente criam e configuram a VM no ambiente KVM. Este arquivo é crucial para entender como cada componente da VM é provisionado e interconectado.

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
resource "libvirt_cloudinit_disk" "cloudinit" {
  for_each = var.vm_configs # Cria um disco Cloud-Init para cada VM definida no mapa `vm_configs`.

  name = "cloudinit-${each.key}.iso" # Nome do arquivo ISO do Cloud-Init, usando o hostname da VM para unicidade.
  pool = "default"                   # Define o pool de armazenamento onde o disco Cloud-Init será criado.

  user_data = templatefile("${path.module}/cloud-init/user_data.yml.tpl", { # Renderiza o template user_data.yml.tpl com variáveis específicas da VM.
    hostname  = each.key
    user_name = each.value.username
    gecos     = each.value.gecos
    groups    = each.value.groups
    ssh_key   = var.ssh_public_key
  })

  network_config = templatefile("${path.module}/cloud-init/network_config.yml.tpl", { # Renderiza o template network_config.yml.tpl com variáveis de rede da VM.
    network_ip    = each.value.network_ip
    nameserver_ip = each.value.nameserver_ip
    route_ip      = each.value.route_ip
  })
}

resource "libvirt_volume" "os_image" {
  for_each = var.vm_configs # Cria um volume de disco para cada VM.

  name   = "base-${each.key}.qcow2" # Nome do volume de disco, usando o hostname da VM.
  pool   = "default"                 # Pool de armazenamento para a imagem do sistema operacional.
  source = each.value.os_image_url   # URL da imagem QCOW2 base a ser copiada para o volume.
  format = "qcow2"                   # Formato da imagem do disco.
}

resource "libvirt_domain" "domain" {
  for_each = var.vm_configs # Cria um domínio Libvirt (VM) para cada configuração de VM.

  name   = each.key         # Nome da VM (hostname).
  memory = each.value.memory # Memória RAM alocada para a VM.
  vcpu   = each.value.vcpu   # Número de vCPUs alocadas para a VM.

  cpu {
    mode = "host-passthrough" # Configura a CPU da VM para usar as capacidades do host, otimizando o desempenho.
  }

  cloudinit = libvirt_cloudinit_disk.cloudinit[each.key].id # Associa o disco Cloud-Init à VM.

  network_interface {
    network_id = var.network_id # Conecta a VM à rede virtual definida no módulo pai.
  }

  disk {
    volume_id = libvirt_volume.os_image[each.key].id # Anexa o volume de disco da imagem do SO à VM.
  }

  console {
    type        = "pty"       # Tipo de console (pseudo-terminal).
    target_port = "0"         # Porta alvo para o console.
    target_type = "virtio"    # Tipo de dispositivo para o console.
  }
}

Explicação Detalhada:

  • resource "libvirt_cloudinit_disk" "cloudinit" { ... }: Este recurso é responsável por criar um disco ISO que contém as configurações iniciais para a VM, utilizando o Cloud-Init. O for_each permite que um disco seja gerado para cada VM definida. Os blocos user_data e network_config utilizam a função templatefile para preencher os templates YML do Cloud-Init com dados dinâmicos, como hostname, usuário, chaves SSH e configurações de rede.
  • resource "libvirt_volume" "os_image" { ... }: Este recurso provisiona um volume de disco no pool de armazenamento KVM. Ele é usado para copiar a imagem base do sistema operacional (QCOW2) de uma URL externa para o ambiente KVM, servindo como o disco principal da VM. O for_each garante que um volume seja criado para cada VM.
  • resource "libvirt_domain" "domain" { ... }: Este é o recurso central que define a máquina virtual KVM (o domínio Libvirt). Ele configura atributos como nome, memória, vCPUs e associa o disco Cloud-Init e o volume da imagem do SO à VM. A configuração cpu { mode = "host-passthrough" } é uma otimização importante para o desempenho da VM, permitindo que ela utilize as extensões de virtualização do processador do host diretamente. As seções network_interface e disk conectam a VM à rede virtual e ao disco de inicialização, respectivamente. O bloco console configura um console serial, útil para depuração e acesso inicial à VM.

1.6. modules/vm/outputs.tf

O arquivo outputs.tf dentro de um módulo define os valores que o módulo irá expor para o módulo pai ou para o usuário final. Isso é útil para obter informações importantes sobre os recursos criados, como endereços IP ou nomes de domínio, que podem ser necessários para interações futuras ou para validação do provisionamento.

1
2
3
4
output "vm_ips" {
  description = "Endereços IP das VMs provisionadas" # Descrição clara do que a saída representa.
  value       = { for k, v in libvirt_domain.domain : k => v.network_interface[0].addresses[0] } # Mapeia o hostname da VM para o seu primeiro endereço IP.
}

Explicação Detalhada:

  • output "vm_ips" { ... }: Este bloco declara uma saída chamada vm_ips. O nome da saída deve ser descritivo e indicar o tipo de informação que ela fornecerá.
  • description: Uma descrição clara do propósito da saída. Isso é fundamental para a documentação do módulo, ajudando outros usuários (ou você mesmo no futuro) a entender o que essa saída representa e como ela pode ser utilizada.
  • value: A expressão Terraform que calcula o valor da saída. Neste caso, utilizamos uma for expression para iterar sobre todos os domínios Libvirt (libvirt_domain.domain) criados pelo módulo. Para cada domínio, ele extrai o key (que é o hostname da VM) e o primeiro endereço IP (v.network_interface[0].addresses[0]) da interface de rede. O resultado é um mapa onde as chaves são os hostnames das VMs e os valores são seus respectivos endereços IP. Isso permite que o módulo pai acesse facilmente os IPs de todas as VMs provisionadas. da primeira interface de rede de cada VM. Isso permite que você obtenha facilmente os IPs de todas as VMs após a implantação.

1.7. modules/vm/variables.tf

Assim como o variables.tf global, este arquivo define as variáveis de entrada específicas para o módulo vm. Essas variáveis permitem que o módulo seja reutilizável e configurável a partir do main.tf do projeto principal.

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
variable "vm_configs" {
  type = map(object({
    username      = string
    gecos         = string
    groups        = list(string)
    network_ip    = string
    nameserver_ip = string
    route_ip      = string
    memory        = number
    vcpu          = number
    os_image_url  = string
  }))
  description = "Um mapa de configurações para cada máquina virtual a ser provisionada por este módulo."
}

variable "network_id" {
  type = string
  description = "O ID da rede Libvirt à qual as VMs serão conectadas."
}

variable "ssh_public_key" {
  type      = string
  sensitive = true
  description = "A chave SSH pública a ser injetada nas VMs para acesso."
}

Explicação:

  • vm_configs: Recebe o mapa de configurações de VMs do main.tf principal. É a principal entrada para o módulo, definindo as características de cada VM a ser criada.
  • network_id: Recebe o ID da rede Libvirt criada no projeto principal, garantindo que as VMs sejam conectadas à rede correta.
  • ssh_public_key: Recebe a chave SSH pública do projeto principal, que será usada para configurar o acesso SSH nas VMs via Cloud-Init.

1.8. modules/vm/versions.tf

O arquivo versions.tf dentro de um módulo especifica as versões mínimas e máximas dos provedores Terraform que este módulo requer. Isso ajuda a garantir a compatibilidade e a evitar problemas quando o módulo é usado em diferentes projetos ou por diferentes colaboradores.

1
2
3
4
5
6
7
8
terraform {
  required_providers {
    libvirt = {
      source  = "dmacvicar/libvirt"
      version = "0.8.3" # Use a mesma versão da raiz
    }
  }
}

Explicação:

  • Este bloco é similar ao versions.tf do projeto raiz, mas se aplica especificamente ao módulo. Ele garante que o provedor libvirt na versão 0.8.3 (ou compatível) esteja disponível para o módulo. A nota “Use a mesma versão da raiz” é um lembrete importante para manter a consistência entre o módulo e o projeto principal.

2. Configuração do Cloud-Init

O Cloud-Init é um pacote padrão da indústria para personalização de máquinas virtuais na primeira inicialização. Ele permite que você injete configurações como usuários, chaves SSH, configurações de rede e scripts de execução inicial nas VMs de forma automatizada. No nosso projeto Terraform, utilizamos templates do Cloud-Init para configurar as VMs provisionadas pelo provedor libvirt.

Os templates do Cloud-Init são arquivos de texto com variáveis (indicadas por ${...}) que o Terraform preenche dinamicamente usando a função templatefile. Temos dois templates principais:

2.1. Configuração de Usuário e SSH (modules/vm/cloud-init/user_data.yml.tpl)

Este template (user_data.yml.tpl) é usado para configurar usuários, chaves SSH e comandos a serem executados na primeira inicialização da VM. Ele é passado para o recurso libvirt_cloudinit_disk no módulo VM.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#cloud-config

users:
  - name: ${user_name}             # Define o nome do usuário a ser criado na VM.
    gecos: ${gecos}               # Informações gerais sobre o usuário (Nome Completo, etc.).
    sudo: ALL=(ALL) NOPASSWD:ALL  # Concede permissões sudo sem a necessidade de senha.
    groups: ${jsonencode(groups)} # Adiciona o usuário aos grupos especificados. A função `jsonencode` é usada para formatar a lista de grupos corretamente para o Cloud-Init.
    shell: /bin/bash              # Define o shell padrão para o usuário.
    lock_passwd: true             # Bloqueia o login por senha para este usuário, forçando o uso de chave SSH.
    ssh_authorized_keys:
      - "${ssh_key}"            # Adiciona a chave SSH pública fornecida para permitir acesso via SSH sem senha.

disable_root: true              # Desabilita o usuário root.
ssh_pwauth: false               # Desabilita a autenticação por senha para SSH em todo o sistema.

runcmd:
  - hostnamectl set-hostname ${hostname} # Define o hostname da VM após a inicialização.

Explicação:

  • O arquivo começa com #cloud-config, que é um cabeçalho obrigatório para arquivos Cloud-Init.
  • A seção users configura um novo usuário com as propriedades especificadas pelas variáveis do template (user_name, gecos, groups, ssh_key).
  • disable_root e ssh_pwauth aumentam a segurança desabilitando o login root direto e a autenticação por senha via SSH.
  • runcmd lista comandos que serão executados uma única vez durante a primeira inicialização da VM. Aqui, usamos hostnamectl para definir o nome da máquina com base na variável ${hostname}.

2.2. Configuração de Rede (modules/vm/cloud-init/network_config.yml.tpl)

Este template (network_config.yml.tpl) é usado para configurar a interface de rede da VM, definindo endereço IP, gateway e servidores DNS. Ele também é passado para o recurso libvirt_cloudinit_disk.

1
2
3
4
5
6
7
8
9
10
11
12
13
#cloud-config
network:
  version: 2
  ethernets:
    ens3:                       # Nome da interface de rede. Pode variar dependendo do sistema operacional da imagem base.
      addresses:
        - ${network_ip}/24      # Define o endereço IP e a máscara de sub-rede para a interface.
      nameservers:
        addresses: 
          - ${nameserver_ip}    # Define o endereço IP do servidor DNS.
      routes:
        - to: 0.0.0.0/0
          via: ${route_ip}      # Define a rota padrão (gateway) para a internet.

Explicação:

  • A seção network configura as interfaces de rede da VM.
  • version: 2 indica a versão do formato de configuração de rede do Cloud-Init.
  • ethernets lista as interfaces Ethernet a serem configuradas. ens3 é um nome comum para a primeira interface de rede em muitas distribuições Linux modernas.
  • addresses, nameservers e routes configuram o endereço IP estático, o servidor DNS e o gateway padrão, respectivamente, utilizando as variáveis fornecidas pelo Terraform (network_ip, nameserver_ip, route_ip).

Validando e Testando Cloud-Init

O tutorial menciona dois comandos que podem ser úteis durante o desenvolvimento e depuração:

  • cloud-init schema --config-file user_data.yml: Este comando (parte das ferramentas do Cloud-Init, que podem precisar ser instaladas na máquina onde você está executando o Terraform ou em uma VM de teste) valida a sintaxe de um arquivo de configuração do Cloud-Init contra seu esquema. É útil para verificar se seus arquivos .yml estão formatados corretamente.
  • terraform console: Este comando inicia um console interativo do Terraform onde você pode avaliar expressões. O exemplo > var.groups mostra como você pode inspecionar o valor de uma variável Terraform, o que é útil para verificar se os dados estão sendo carregados e formatados como esperado antes de serem passados para os templates ou recursos.

3. Configurações Sensíveis (terraform.tfvars)

O arquivo terraform.tfvars é crucial para o seu projeto Terraform, pois é onde você define os valores para as variáveis de entrada que não possuem um valor padrão ou que contêm informações sensíveis. É de extrema importância que este arquivo NUNCA seja versionado em sistemas de controle de versão como o Git, pois ele pode conter chaves SSH, senhas ou outros dados confidenciais.

3.1. Crie terraform.tfvars a partir do Exemplo

Para começar, você deve criar seu próprio arquivo terraform.tfvars na raiz do projeto. O arquivo terraform.tfvars.example serve como um modelo para guiá-lo sobre quais variáveis precisam ser definidas e o formato esperado. Copie o conteúdo do terraform.tfvars.example para um novo arquivo chamado terraform.tfvars:

1
cp terraform.tfvars.example terraform.tfvars

Agora, edite o arquivo terraform.tfvars que você acabou de criar, preenchendo os valores conforme suas necessidades. Abaixo, um exemplo de como seu terraform.tfvars pode se parecer:

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
ssh_public_key = "ssh-ed25519 AAAAC... user@host" # Substitua pela sua chave SSH pública real

vms = {
  "debian-vm" = { # Nome da VM (hostname)
    username      = "suporte"      # Nome de usuário para acesso SSH
    gecos         = "Suporte User" # Informações gerais do usuário
    groups        = ["users", "sudo"] # Grupos aos quais o usuário pertencerá
    network_ip    = "10.64.0.10"   # Endereço IP estático para a VM
    nameserver_ip = "10.64.0.1"    # Endereço IP do servidor DNS
    route_ip      = "10.64.0.1"    # Endereço IP do gateway padrão
    memory        = 4096           # Memória RAM em MB
    vcpu          = 4              # Número de vCPUs
    os_image_url  = "/home/gean/kvm/templates/debian-12-amd64.qcow2" # Caminho completo para a imagem base do SO
  },
  "oracle-vm" = {
    username      = "suporte"
    gecos         = "Suporte User"
    groups        = ["users", "wheel"]
    network_ip    = "10.64.0.11"
    nameserver_ip = "10.64.0.1"
    route_ip      = "10.64.0.1"
    memory        = 4096
    vcpu          = 4
    os_image_url  = "/home/gean/kvm/templates/ol9-amd64.qcow2"
  }
}

# Configurações opcionais (podem ser sobrescritas aqui, se necessário)
libvirt_uri       = "qemu:///system"
network_name      = "tfnet"
network_mode      = "nat"
network_addresses = "10.64.0.0/24"

Entendendo o Bloco vms:

O bloco vms é um mapa de objetos, onde cada chave representa o hostname de uma máquina virtual que você deseja provisionar. Para cada VM, você define um conjunto de atributos como username, network_ip, memory, vcpu, e o caminho para a os_image_url. Certifique-se de que o os_image_url aponte para uma imagem QCOW2 válida em seu sistema KVM.

Nota sobre Imagens Base: Você pode baixar imagens template prontas (ex: wget -O ~/kvm/templates/debian-12-amd64.qcow2 https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2) ou criar suas próprias imagens personalizadas. Para a criação de templates personalizados, você pode consultar os seguintes guias:

Gerenciamento de Segredos em Produção:

Para ambientes de produção, o uso de terraform.tfvars para dados sensíveis não é a prática mais recomendada. Considere integrar soluções de gerenciamento de segredos como:

  • HashiCorp Vault: Uma ferramenta para armazenar e gerenciar segredos de forma centralizada.
  • Variáveis de Ambiente em Pipelines de CI/CD: Injetar segredos como variáveis de ambiente seguras durante a execução do Terraform em pipelines de integração contínua/entrega contínua.

4. Versionamento com .gitignore

O uso correto do .gitignore é fundamental para qualquer projeto versionado com Git, especialmente em projetos Terraform que lidam com arquivos de estado (.tfstate) e variáveis sensíveis (terraform.tfvars). Estes arquivos não devem ser incluídos no controle de versão por questões de segurança e para evitar conflitos.

Crie ou edite o arquivo .gitignore na raiz do## 4. Versionamento com .gitignore

O uso correto do .gitignore é fundamental para qualquer projeto versionado com Git, especialmente em projetos Terraform que lidam com arquivos de estado (.tfstate) e variáveis sensíveis (terraform.tfvars). Estes arquivos não devem ser incluídos no controle de versão por questões de segurança e para evitar conflitos.

Crie ou edite o arquivo .gitignore na raiz do seu projeto com o seguinte conteúdo:

# Terraform
.terraform/
*.tfstate
*.tfstate.backup
crash.log
*.tfvars
*.tfvars.json

# Providers
.terraform.lock.hcl

# Local Configuration
.DS_Store
*.log
*.out

Explicação:

  • .terraform/: Ignora o diretório .terraform, que contém os plugins dos provedores e outros dados internos do Terraform. Este diretório é gerado automaticamente pelo terraform init.
  • *.tfstate: Ignora o arquivo de estado do Terraform. O arquivo .tfstate contém o mapeamento entre os recursos reais da sua infraestrutura e a configuração do Terraform. Ele pode conter informações sensíveis e deve ser gerenciado por um backend remoto (como Terraform Cloud, S3, etc.) em ambientes de equipe.
  • *.tfstate.backup: Ignora os backups automáticos do arquivo de estado que o Terraform cria.
  • crash.log: Ignora logs de crash do Terraform.
  • *.tfvars e *.tfvars.json: Ignora os arquivos de variáveis, como terraform.tfvars, que contêm valores específicos do ambiente e, frequentemente, informações sensíveis (como chaves SSH, senhas). É crucial que esses arquivos nunca sejam versionados!
  • .terraform.lock.hcl: Ignora o arquivo de bloqueio de provedores, que garante que a mesma versão dos provedores seja usada por todos os colaboradores.
  • .DS_Store, *.log, *.out: Ignora arquivos de sistema macOS, logs genéricos e arquivos de saída que não devem ser versionados.

Explicação:

  • .terraform/: Ignora o diretório .terraform, que contém os plugins dos provedores e outros dados internos do Terraform. Este diretório é gerado automaticamente pelo terraform init.
  • *.tfstate: Ignora o arquivo de estado do Terraform. O arquivo .tfstate contém o mapeamento entre os recursos reais da sua infraestrutura e a configuração do Terraform. Ele pode conter informações sensíveis e deve ser gerenciado por um backend remoto (como Terraform Cloud, S3, etc.) em ambientes de equipe.
  • *.tfstate.backup: Ignora os backups automáticos do arquivo de estado que o Terraform cria.
  • crash.log: Ignora logs de crash do Terraform.
  • *.tfvars e *.tfvars.json: Ignora os arquivos de variáveis, como terraform.tfvars, que contêm valores específicos do ambiente e, frequentemente, informações sensíveis (como chaves SSH, senhas). É crucial que esses arquivos nunca sejam versionados!
  • .terraform.lock.hcl: Ignora o arquivo de bloqueio de provedores, que garante que a mesma versão dos provedores seja usada por todos os colaboradores.
  • .DS_Store, *.log, *.out: Ignora arquivos de sistema macOS, logs genéricos e arquivos de saída que não devem ser versionados.

5.2. Planeje a Infraestrutura

1
terraform plan

5.3. Aplique as Mudanças

1
terraform apply

6. Acessando a VM

6.1. Acesso via SSH

1
2
3
ssh -i ~/.ssh/tfvms <user_name>@<vm_ip>
# Exemplo:
ssh -i ~/.ssh/tfvms suporte@10.64.0.10

7. Destruindo a infraestrutura

7.1. Destroi a infra

1
terraform destroy

Conclusão

Este tutorial detalhou o processo de provisionamento de infraestrutura KVM utilizando Terraform e módulos reutilizáveis. Através da modularização, demonstramos como é possível criar um ambiente de virtualização robusto, escalável e de fácil manutenção. A automação de tarefas repetitivas, como a criação de máquinas virtuais e a configuração de redes, não só otimiza o tempo dos administradores e engenheiros de DevOps, mas também minimiza erros e garante a consistência do ambiente.

Ao longo deste guia, você aprendeu a estruturar seu projeto Terraform, a configurar provedores e variáveis, e a utilizar módulos para encapsular a lógica de provisionamento de VMs. A integração com o Cloud-Init para personalização inicial das máquinas virtuais é um diferencial que permite um alto grau de automação desde o primeiro boot. A capacidade de definir redes virtuais e gerenciar o ciclo de vida completo das VMs diretamente do código-fonte é um testemunho do poder da Infraestrutura como Código.

Esperamos que este tutorial sirva como um ponto de partida sólido para suas próprias implementações de KVM com Terraform. Encorajamos você a explorar ainda mais as capacidades do Terraform, como a gestão de estado remoto, a integração com sistemas de CI/CD e a criação de módulos mais complexos para atender às suas necessidades específicas. A jornada para uma infraestrutura totalmente automatizada é contínua, e o Terraform é uma ferramenta poderosa para guiá-lo nesse caminho. Continue experimentando, aprendendo e otimizando seus fluxos de trabalho para construir ambientes cada vez mais eficientes e resilientes.

This post is licensed under CC BY 4.0 by the author.