View on GitHub

emersonprado.github.io

Página inicial
English

Vagrantfiles eficientes – Parte 2 – Vagrant é Ruby!

Emerson Prado - 26/05/2019

Na parte 1 desta série de artigos, eu insisti na ideia de que Vagrant é Ruby. Agora vamos ver pra que isso serve.

  1. TL;DR
  2. O problema - repetição de código
  3. A solução - variáveis, laços e condicionais
    1. Arrays e Hashes
    2. O laço each
    3. Condicionais
  4. Onde isso entra no Vagrantfile?
  5. O céu é o limite?

TL;DR

Se você já sabe o básico de Ruby, e usa regularmente arrays, hashes, laços com each e condicionais, talvez você possa pular esta parte dos artigos, e fazer tudo funcionar com o resumo abaixo.

Você já tem um Vagrantfile funcional, mas com uma boa quantidade de código repetido. Vejamos como nos livrar das repetições:

Iterando um array pra criar máquinas virtuais similares. Exemplo:

  Vagrant.configure("2") do |config|
    [
      'vm_1',
      'vm_2',
      'vm_3'
    ].each do |name|
      config.vm.define name do |vm|
        <Configurações>
      end
    end
  end
  

Iterando um hash pra criar máquinas virtuais com um atributo variável. Exemplo:

  Vagrant.configure("2") do |config|
    {
      'vm_1' => 'ARTACK/debian-jessie',
      'vm_2' => 'ARTACK/debian-jessie',
      'vm_3' => 'centos/7'
    }.each do |name, box|
      config.vm.define name do |vm|
        vm.vm.box = box
      end
    end
  end
  

Iterando um hash de hashes pra criar máquinas virtuais com múltiplos atributos variáveis. Exemplo:

  Vagrant.configure("2") do |config|
    {
      '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',
        :ip => '192.168.1.4'
      }
    }.each do |name, confs|
      config.vm.define name do |vm|
        vm.vm.box = confs[:box]
        vm.vm.network "private_network", ip: confs[:ip]
      end
    end
  end
  

Usando condicionais para incluir atributos em um subconjunto das máquinas vituais. Exemplo (ip, neste caso):

  Vagrant.configure("2") do |config|
    {
      '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',
      }
    }.each do |name, confs|
      config.vm.define name do |vm|
        vm.vm.box = confs[:box]
        vm.vm.network "private_network", ip: confs[:ip] if confs.has_key?(:ip)
      end
    end
  end
  

Agora você deve estar pensando em várias formas de usar hashes aninhados e condicionais em inúmeras situações possíveis. Lembre sempre que os hashes precisam ficar legíveis para não programadores - o Vagrant serve pra tornar o gerenciamento de MVs mais simples, não nível Einstein. Agora vamos pra parte 3 pra começar a nos livrar da bagunça visual de hashes enormes pendurados no início dos laços.

O problema - repetição de código

Imagine que você vai criar um ambiente Vagrant com várias máquinas virtuais. Na forma mostrada na primeira parte - e que eu usava quando comecei a usar o Vagrant - o Vagrantfile teria que conter o mesmo código para todas. Exemplo:

  Vagrant.configure("2") do |config|
    config.vm.define 'vm_1' do |vm|
      vm.vm.box = <Box>
      <Configurações>
    end
    config.vm.define 'vm_2' do |vm|
      vm.vm.box = <Box>
      <Configurações>
    end
    ...
    config.vm.define 'vm_N' do |vm|
      vm.vm.box = <Box>
      <Configurações>
    end
  end
  

O código feito assim tende a ficar muito longo, e trabalhoso pacas para criar (copia-cola-edita-cola-edita...). Também fica ruim para manter - se for adicionar MVs, tem que repetir todo o processo. Pra mudar uma configuração comum, é necessário editar todas as MVs. Fora que, pra achar um erro aí...

A solução - variáveis, laços e condicionais

Caso você seja um desenvolvedor (e provavelmente é), já percebeu que o código acima tem um padrão de repetição, com mudanças previsíveis em pontos chave. Ruby é uma linguagem de programação, então...

Vamos rever alguns conceitos do Ruby que vão ajudar muito. Não será uma aula de verdade, mas só uma pincelada no que vamos usar.

Usaremos o comando ruby -e, que executa código Ruby no shell

Arrays e Hashes

Arrays são conjuntos de objetos. Simples assim: uma lista com vários itens. Eles devem ser especificados entre colchetes, separados por vírgulas.

Hashes são conjuntos indexados. Cada item tem um índice, chamado chave, e um valor associado. Eles devem ser especificados entre chaves, separados por vírgulas.

Usaremos o método class, que retorna a classe da qual o objeto é instância
  $ ruby -e 'puts [-5, 3.14, "Text", :simbolo].class'
  Array

  $ ruby -e 'puts ({1 => "b", "c" => 5.5, :outra => "coisa"}.class)'
  Hash
  

Lembrando que no Ruby tudo é objeto. Então não faz sentido falarmos em "tipos". Array e Hash são classes.

E os arrays e hashes podem ser aninhados (dividindo em linhas para melhor visualização):

  $ ruby -e 'puts [
    1,
    [2, 3, 4],
    {1 => "a"}
  ].class'
  Array

  $ ruby -e 'puts ({
    1 => [2, 3, 4],
    "c" => {1 => "a"},
    [1, 2] => "f",
    {1 => "a"} => 5
  }.class)'
  Hash
  

O laço each

Podemos criar laços de repetição muito facilmente no Ruby usando o método each. Ao iterar um array, o laço se repete, com uma variável armazenando cada item em uma repetição. Ao iterar um hash, colocamos as chaves em uma variável, e os valores em outra.

Para invocar o método, usamos a sintaxe comum de linguagens orientadas a objeto: <Objeto>.<Método>, ou seja, <Array/Hash>.each. A sintaxe do método each é:

  <Objeto>.each do |<variável(is)>|
    <Bloco utilizando a(s) variável(is)>
  end
  

Demonstrando uma iteração com array e outra com hash:

  $ ruby -e '
    [1, 2, 3].each do |var|
      puts var
    end
  '
  1
  2
  3

  $ ruby -e '
    {1 => "a", 2 => "b", 3 => "c"}.each do |key, value|
      puts "Key: #{key} - Value: #{value}"
    end
  '
  Key: 1 - Value: a
  Key: 2 - Value: b
  Key: 3 - Value: c
  

Condicionais

Condicionais são a mesma coisa em qualquer linguagem: você executa um bloco de instruções se um conjunto de condições forem verdadeiras. Em Ruby, as instruções condicionais são if, elsif, else e unless:

  $ ruby -e '
    puts "Favor digitar a idade"
    age = gets().to_i
    unless age < 0 then
      if age >= 18 then
        puts "Adulto"
      elsif age >= 12
        puts "Adolescente"
      else
        puts "Criança"
      end
    else
      puts "?"
    end
  '
  

Onde isso entra no Vagrantfile?

Usando estes recursos, podemos transformar um código repetido em um código único, geral, com variáveis para incluir os dados.

Você pode testar os códigos abaixo com vagrant status, vagrant up, vagrant ssh, etc., depois apagar as MVs com vagrant destroy. Na dúvida, use vagrant help.

Por exemplo, vamos criar várias MVs iguais, trocando só o nome:

  Vagrant.configure("2") do |config|
    config.vm.define 'vm_1' do |vm|
      vm.vm.box = 'ARTACK/debian-jessie'
    end
    config.vm.define 'vm_2' do |vm|
      vm.vm.box = 'ARTACK/debian-jessie'
    end
    config.vm.define 'vm_3' do |vm|
      vm.vm.box = 'ARTACK/debian-jessie'
    end
  end
  

O código é sempre o mesmo, só mudando o nome da MV. Que tal um laço iterando um array com os nomes, executando um só trecho de código?

  Vagrant.configure("2") do |config|
    [
      'vm_1',
      'vm_2',
      'vm_3'
    ].each do |nome|
      config.vm.define nome do |vm|
        vm.vm.box = 'ARTACK/debian-jessie'
      end
    end
  end
  

Além do código mais enxuto, note a facilidade de incluir, excluir e alterar máquinas virtuais, só alterando o array.

E se as MVs não forem iguais? Simples: basta fazer um hash, tendo os nomes como chaves, e a configuração variável como valor:

  Vagrant.configure("2") do |config|
    {
      'vm_1' => 'ARTACK/debian-jessie',
      'vm_2' => 'ARTACK/debian-jessie',
      'vm_3' => 'centos/7'
    }.each do |nome, box|
      config.vm.define nome do |vm|
        vm.vm.box = box
      end
    end
  end
  

E se houver mais configurações variáveis? Bom, tanto as chaves quanto os valores de um hash podem ser praticamente qualquer objeto. E se os valores fossem outros hashes, com as configurações de cada MV? Na iteração, vamos chamar de 'confs' estes hashes internos:

  Vagrant.configure("2") do |config|
    {
      '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',
        :ip => '192.168.1.4'
      }
    }.each do |nome, confs|
      config.vm.define nome do |vm|
        vm.vm.box = confs[:box]
        vm.vm.network "private_network", ip: confs[:ip]
      end
    end
  end
  

Se estiver complicado de entender o hash de hashes, vamos visualizar na linha de comando:

  $ ruby -e '
    {
      "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",
        :ip ="192.168.1.4"
      }
    }.each do |nome, confs|
      puts "Chave: #{nome} - Valor: #{confs}"
    end
  '
  Chave: vm_1 - Valor: {:box=>"ARTACK/debian-jessie", :ip=>"192.168.1.2"}
  Chave: vm_2 - Valor: {:box=>"ARTACK/debian-jessie", :ip=>"192.168.1.3"}
  Chave: vm_3 - Valor: {:box=>"centos/7", :ip=>"192.168.1.4"}
  

Veja os hashes de configurações como valores de cada item do hash principal.

E se nem todas as MVs usam uma determinada configuração? Aqui entram os condicionais:

  Vagrant.configure("2") do |config|
    {
      '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',
      }
    }.each do |nome, confs|
      config.vm.define nome do |vm|
        vm.vm.box = confs[:box]
        vm.vm.network "private_network", ip: confs[:ip] if confs.has_key?(:ip)
      end
    end
  end
  

Aqui, a ausência da chave :ip no hash de configurações da MV 'mv_3' faz com que a diretiva if confs.has_key?(:ip) seja falsa, então a configuração vm.network não é incluída. Ou seja, não se inclui IP de rede privada na MV 'mv_3'.

O céu é o limite?

É possível também criar configurações múltiplas (como vários IPs na mesma MV), usando arrays como valor da configuração, usar laços para criar configurações globais, e assim por diante. O limite é a legibilidade - muitos níveis de aninhamento nos hashes acabam dificultando a compreensão dos dados, e qualquer análise ou alteração seria muito trabalhosa. E a intenção aqui é exatamente o contrário.


Agora o código está mais organizado, mas ainda meio "embolado" com o hash enorme no início. Hora de ir pra parte 3.