English
Vagrantfiles eficientes – Parte 4 – Integração de código e dados
Emerson Prado - 02/08/2020
Na parte 3 desta série de artigos, vimos o formato YAML e como ele armazena dados. Vamos usar este formato pra, finalmente, deixar nosso ambiente fácil de ler e manter.
TL;DR
Se você já sabe converter de objetos Ruby para arquivos YAML e vice-versa, talvez você possa pular esta parte dos artigos, e fazer tudo funcionar com o resumo abaixo.
Na parte 2, ficamos com um hash com dados de todas as MVs, e um código com um laço iterando a partir do hash. Vamos usar o método dump
do Ruby pra criar o arquivo externo etc/mvs.yaml
, no formato YAML, com este hash:
mkdir etc ruby -e " require 'yaml' puts(YAML.dump({ 'vm_1' => { :box => 'ARTACK/debian-jessie', :ip => '192.168.1.2' }, 'vm_2' => { :box => 'ARTACK/debian-jessie', :ip => '192.168.1.3' }, 'vm_3' => { :box => 'centos/7' } })) " > etc/mvs.yaml
Nas minhas experiências, eu costumo precisar de mais algumas configurações do VirtualBox. Então incluí no arquivo YAML gerado.
cat etc/mvs.yaml --- vm_1: :box: ARTACK/debian-jessie # Gerado no comando dump :ram: 256 # Incluído manualmente :ip: 192.168.1.2 # Gerado no comando dump :custom: # Incluído manualmente '--usb': 'off' '--usbehci': 'off' '--usbxhci': 'off' '--vrde': 'off' vm_2: :box: ARTACK/debian-jessie :ram: 256 :ip: 192.168.1.3 :custom: '--usb': 'off' '--usbehci': 'off' '--usbxhci': 'off' '--vrde': 'off' vm_3: :box: centos/7 :cpu: 2 :custom: '--usb': 'off' '--usbehci': 'off' '--usbxhci': 'off' '--vrde': 'off'
Agora, no Vagrantfile, vamos carregar este arquivo em uma variável, que terá o mesmo hash de antes. Depois, usar esta variável no laço, com as configurações adicionais:
require 'yaml' MVS = YAML.load_file('etc/mvs.yaml') # Carrega arquivo YAML em variável Vagrant.configure("2") do |config| MVS.each do |name, confs| # Laço a partir da variável config.vm.define name do |vm| config.vbguest.auto_update = false vm.vm.box = confs[:box] vm.vm.network "private_network", ip: confs[:ip] if confs.has_key?(:ip) # Configurações adicionais vm.vm.provider 'virtualbox' do |virtualbox| virtualbox.memory = confs[:ram] if confs.has_key?(:ram) virtualbox.cpus = confs[:cpu] if confs.has_key?(:cpu) confs[:custom].each do |custom, value| virtualbox.customize ['modifyvm', :id, custom, value] end if confs.has_key?(:custom) end end end end
Você deve ter percebido (palmas!) que o arquivo de MVs tem praticamente os mesmos dados para MVs que usam o mesmo box. Vamos dividir em 2 arquivos - MVs e boxes:
Arquivo dos boxes - etc/boxes.yaml
--- debian_jessie: :name: ARTACK/debian-jessie :ram: 256 :custom: '--usb': 'off' '--usbehci': 'off' '--usbxhci': 'off' '--vrde': 'off' centos_7: :name: centos/7 :cpu: 2 :custom: '--usb': 'off' '--usbehci': 'off' '--usbxhci': 'off' '--vrde': 'off'
Arquivo das MVs - etc/mvs.yaml
--- vm_1: :box: debian_jessie # Configurações da MV incluem o box usado :ip: 192.168.1.2 vm_2: :box: debian_jessie :ip: 192.168.1.3 vm_3: :box: centos_7
Então, no Vagrantfile
require 'yaml' BOXES = YAML.load_file('etc/boxes.yaml') # Variável dos boxes MVS = YAML.load_file('etc/mvs.yaml') # Variável das MVs Vagrant.configure("2") do |config| # Laço a partir da variável das MVs MVS.each do |name, confs| config.vm.define name do |vm| config.vbguest.auto_update = false # Configurações do box do arquivo dos boxes box = BOXES[confs[:box]] vm.vm.box = box[:name] vm.vm.network "private_network", ip: confs[:ip] if confs.has_key?(:ip) vm.vm.provider 'virtualbox' do |virtualbox| virtualbox.memory = box[:ram] if box.has_key?(:ram) virtualbox.cpus = box[:cpu] if box.has_key?(:cpu) box[:custom].each do |custom, value| virtualbox.customize ['modifyvm', :id, custom, value] end if box.has_key?(:custom) end end end end
Agora, terminamos com 3 arquivos pequenos e organizados, cada um tendo só dados ou só código, e todos bem simples de entender e manter. Ou seja: missão cumprida!
Complexidade é o limite. Você pode ter só um arquivo de dados, ou dividir os dados em vários arquivos. Procure equilibrar concisão e simplicidade. O objetivo é facilitar a compreensão e a manutenção do conjunto, inclusive por outros, muitas vezes não programadores. Bom senso é seu amigo!
Ruby e YAML
Na parte 3, eu disse que Ruby e YAML são amigos íntimos. Na língua de programadores, isso quer dizer que existe um módulo especializado, na biblioteca padrão do Ruby, que trata YAML. Em palavras mais do mundo real, quer dizer que dá pra usar métodos de conversão pra YAML (e mais) instalando nada mais que o próprio Ruby. É só carregar o módulo antes de usar as funções, com require 'yaml'
.
Conversão Ruby pra YAML
Pra converter objetos Ruby em YAML, usamos o método dump
, que devolve uma string YAML com o objeto passado.
ruby -e ' require "yaml" puts(YAML.dump([ "Array com", 3, "Itens" ])) ' --- - Array com - 3 - Itens ruby -e ' require "yaml" puts(YAML.dump({ "Primeiro" => "Elemento do hash", "Segundo" => "Elemento do hash", "Terceiro" => "Elemento do hash" })) ' --- Primeiro: Elemento do hash Segundo: Elemento do hash Terceiro: Elemento do hash
Conversão YAML pra Ruby
Pro vice-versa, converter YAML em objetos Ruby, usamos o método load
, que carrega a string YAML passada em um objeto Ruby. Vou tirar a indentação das seções com dados abaixo porque o YAML não permite indentação a gosto (observação: YAML é exigente).
ruby -e ' require "yaml" YAML.load(" --- - 1 - 2 - 3 ").each do |item| puts("Item: #{item}") end ' Item: 1 Item: 2 Item: 3 ruby -e ' require "yaml" YAML.load(" --- 1: Um 2: Dois 3: Três ").each do |chave, valor| puts("Chave: #{chave} - Valor: #{valor}") end ' Chave: 1 - Valor: Um Chave: 2 - Valor: Dois Chave: 3 - Valor: Três
Nosso Vagrantfile
Nossa missão agora é transformar o hash enorme que ficou pendurado na parte 2 em um belo arquivo só com dados, e deixar o Vagrantfile só com código.
É claro que o método dump
pode fazer a conversão. Mas com uma diferença: o resultado precisa ir pra um arquivo, em vez da tela. É só redirecionar a saída do comando para um arquivo. Vamos batizar o arquivo de mvs.yaml
. Pra mais limpeza, vamos criar um diretório para este arquivo de configurações. Não imagino qualquer nome melhor que etc
.
mkdir etc ruby -e " require 'yaml' puts(YAML.dump({ 'vm_1' => { :box => 'ARTACK/debian-jessie', :ip => '192.168.1.2' }, 'vm_2' => { :box => 'ARTACK/debian-jessie', :ip => '192.168.1.3' }, 'vm_3' => { :box => 'centos/7', } })) " > etc/mvs.yaml cat etc/mvs.yaml --- vm_1: :box: ARTACK/debian-jessie :ip: 192.168.1.2 vm_2: :box: ARTACK/debian-jessie :ip: 192.168.1.3 vm_3: :box: centos/7
Depois, pra carregar o objeto no Vagrantfile, é só fazer a conversão inversa, com a mesma diferença: vamos carregar a string do arquivo, em vez de digitar. Simples: e só trocar o método load
pelo load_file
.
require 'yaml' MVS = YAML.load_file('etc/mvs.yaml') # Carrega arquivo YAML na variável Vagrant.configure("2") do |config| MVS.each do |nome, confs| # Itera a partir da variável config.vm.define nome do |vm| config.vbguest.auto_update = false vm.vm.box = confs[:box] vm.vm.network "private_network", ip: confs[:ip] if confs.has_key?(:ip) end end end
Mais configurações
É claro que uma máquina virtual não é feita só de nome e IPs. Que tal mexer, por exemplo, nas configurações de RAM e/ou CPU?
require 'yaml' MVS = YAML.load_file('etc/mvs.yaml') Vagrant.configure("2") do |config| MVS.each do |nome, confs| config.vm.define nome do |vm| config.vbguest.auto_update = false vm.vm.box = confs[:box] vm.vm.network "private_network", ip: confs[:ip] if confs.has_key?(:ip) vm.vm.provider 'virtualbox' do |virtualbox| virtualbox.memory = confs[:ram] if confs.has_key?(:ram) virtualbox.cpus = confs[:cpu] if confs.has_key?(:cpu) end end end end
Então, o arquivo de dados fica:
--- vm_1: :box: ARTACK/debian-jessie :ram: 256 :ip: 192.168.1.2 vm_2: :box: ARTACK/debian-jessie :ram: 256 :ip: 192.168.1.3 vm_3: :box: centos/7 :cpu: 2
No meu caso, também precisei modificar configurações específicas do VirtualBox. No Vagrant, a diretiva que faz isso é a virtualbox.customize
.
require 'yaml' MVS = YAML.load_file('etc/mvs.yaml') Vagrant.configure("2") do |config| MVS.each do |nome, confs| config.vm.define nome do |vm| config.vbguest.auto_update = false vm.vm.box = confs[:box] vm.vm.network "private_network", ip: confs[:ip] if confs.has_key?(:ip) vm.vm.provider 'virtualbox' do |virtualbox| virtualbox.memory = confs[:ram] if confs.has_key?(:ram) virtualbox.cpus = confs[:cpu] if confs.has_key?(:cpu) confs[:custom].each do |custom, valor| virtualbox.customize ['modifyvm', :id, custom, valor] end if confs.has_key?(:custom) end end end end
Mudamos o código, que continua independente dos dados (dados fora do código!). Agora, no arquivo de dados, vamos desabilitar USB e a VirtualBox Remote Desktop Extension, que já me causaram problemas em algumas MVs:
As configurações personalizadas exigem nomes e valores - ou seja, um hash como valor da chave ':custom'
--- vm_1: :box: ARTACK/debian-jessie :ram: 256 :ip: 192.168.1.2 :custom: '--usb': 'off' '--usbehci': 'off' '--usbxhci': 'off' '--vrde': 'off' vm_2: :box: ARTACK/debian-jessie :ram: 256 :ip: 192.168.1.3 :custom: '--usb': 'off' '--usbehci': 'off' '--usbxhci': 'off' '--vrde': 'off' vm_3: :box: centos/7 :cpu: 2 :custom: '--usb': 'off' '--usbehci': 'off' '--usbxhci': 'off' '--vrde': 'off'
Mais arquivos
Eu ouvi você dizer "Peraí! Os arquivos de dados estão muito repetitivos!". Fiquei orgulhoso de você!
Sim, muitas configurações serão parecidas. Todas as MVs de um mesmo box têm, pelo menos, o mesmo nome e versão de box. Às vezes, os mesmos recursos e/ou configurações personalizadas. Pra que repetir configurações de MVs se podemos ter um arquivo pros próprios boxes?
Arquivo de dados dos boxes
--- debian_jessie: :nome: ARTACK/debian-jessie :ram: 256 :custom: '--usb': 'off' '--usbehci': 'off' '--usbxhci': 'off' '--vrde': 'off' centos_7: :nome: centos/7 :cpu: 2 :custom: '--usb': 'off' '--usbehci': 'off' '--usbxhci': 'off' '--vrde': 'off'
Arquivo de dados das MVs
--- vm_1: :box: debian_jessie :ip: 192.168.1.2 vm_2: :box: debian_jessie :ip: 192.168.1.3 vm_3: :box: centos_7
Então, no Vagrantfile:
require 'yaml' BOXES = YAML.load_file('etc/boxes.yaml') MVS = YAML.load_file('etc/mvs.yaml') Vagrant.configure("2") do |config| MVS.each do |nome, confs| config.vm.define nome do |vm| config.vbguest.auto_update = false box = BOXES[confs[:box]] vm.vm.box = box[:nome] vm.vm.network "private_network", ip: confs[:ip] if confs.has_key?(:ip) vm.vm.provider 'virtualbox' do |virtualbox| virtualbox.memory = box[:ram] if box.has_key?(:ram) virtualbox.cpus = box[:cpu] if box.has_key?(:cpu) box[:custom].each do |custom, value| virtualbox.customize ['modifyvm', :id, custom, value] end if box.has_key?(:custom) end end end end
Não há fórmula mágica que determine quantos arquivos e o que colocar em cada um. Use o bom senso, equilibrando flexibilidade e simplicidade.
Agora temos 3 arquivos com menos de 20 linhas cada um, todos fáceis de ler, e que resultam em 3 MVs com configurações múltiplas de SO, RAM, CPU, IP e particularidades do virtualizador. E que são fáceis de mudar, expandir e/ou reutilizar. Satisfeito? Eu também.
E depois?
Que outros dados podem ser colocados em arquivos separados para o código usar? Talvez algumas configurações de hardware, rede, algum provisionamento?
Procure padrões de repetição. Só conseguimos começar a separar dados de código quando ficou claro que o código era praticamente o mesmo para todas as MVs. Pudemos criar arquivos com dados dos boxes porque vimos que as configurações das MVs também se repetiam. É por aí.
Isso não quer dizer que seja uma boa ideia colocar cada mínima repetição em um arquivo separado. Dependendo do caso, você pode acabar com arquivos demais, com conteúdo de menos, e desenvolvendo sobre um emaranhado de arquivos e variáveis.
O que você precisa ter sempre em mente é que dividir dados em arquivos deve deixar a manutenção mais fácil. Outros, às vezes não programadores Ruby, e até você meses depois, precisam conseguir (re)usar seu Vagrantfile. Se você mudou a arquitetura dos dados, e desconfia que ficou mais complicada, provavelmente ficou mesmo - Principalmente pra quem não criou os arquivos. Refaça ou desfaça.
Agora que já sabe por que e como separar os dados do código, mãos à obra! Bons Vagrantfiles!