[0] ~ Qualcosa di Dev&Ops: vagrant multimachine setup
Tags: devops, vagrant, yaml
Published: 2020-11-11 23:37:10

[EN_version_TBD]

Da circa un anno a questa parte ricopro il ruolo di DevOps. O, come ironicamente ci piace soprannominarci nel settore, YAML engineer.

Che poi è una definizione che mi sta stretta: è solo quando la mia giornata lavorativa va male, che passo 8 ore al giorno ad arrovellarmi tra una ventina di docker-compose(.yml) e, da poco, anche sui deployment di Kubernetes. Ma, a dirla tutta, preferisco i TOML. Che sono utilizzati poco, molto poco. Quasi niente.

Se qualcuno ancora si stesse chiedendo quali benefici può portare la figura del Devops ad una ipotetica azienda a conduzione familiare, risponderei: _automatizza tutt'cos_!

O, pensandoci su: 

> la persona o il team che traghetta quell'incubo di applicazione, uscita da un film dell'orrore, nel mondo delle _best practices_. Un mondo fatto di: ti automatizzo la build, ti segnalo eventuali bug di sicurezza o quantomeno ti faccio rilasciare un codice ~~manut~~...~~guard~~...capibile., ti standardizzo i test e ti faccio rilasciare il software in produzione con due click.


In poche parole: il massimo risultato con il minimo sforzo. Il sogno bagnato di ogni sfaticato. Incluso me.


La giornata tipo di un devops non è fatta solo di YAML e di Kubernetes, però. Ma anche di passione, emozioni (negative) e automatismi. E come ogni professionista del settore IT, anche noi abbiamo un coltellino ~~cines~~svizzero con 40 funzionalità.

A molti di noi, in ufficio, vengono assegnati dei portatili su cui gira Windows. Nel nostro lavoro, però, automatizzare e rilasciare è una cosa che preferiamo delegare al buon vecchio linux con le sue primitive posix e bash, che tra una __find__, un __awk__ ed un __zcat__ macina anche i sassi.

Microsoft ha finalmente capito quale sia il lato corretto della corrente da seguire, e finalmente ci ha deliziato con strumenti comodi e pratici: VScode, WSL2, Windows Terminal, per citarne alcuni.

I Devops, come si è capito, sono svogliati, non si fidano dei Dev e quindi gli piace automatizzare. Ma sopratutto, gli piace che una cosa possa essere configurata, installata e riutilizzata. Spesso gli ambienti di dev (e non solo quelli) diventano così confusi e pieni di monnezza, che l'unica soluzione sensata sembra essere il formattone riparatore.

4-5 anni fa, uno sviluppatore smart alle prime armi nell'industria del software avrebbe potuto pensare: ok, installo Vbox, scarico una immagine da osboxes, spendo qualche giornata a vedere tutorial su come configurare il maledetto bridge tra vbox e windows, e dopo aver ripetuto manualmente le configurazioni 15 volte sono pronto per lavorare.

Il tempo passa e Microsoft gli va incontro: Ora può semplicemente installare una ricca Ubuntu dallo store di Windows. Questa gira su WSL, ha le cartelle condivise che può usare da Windows, sta sullo stesso network di windows e pertanto non deve impazzire a configurarlo, installa npm/maven/go/sarcazzo e inizia a lavorare. C'è solo la shell. Pazienza, deve fare solo mv, cp e lanciare la build e l'esecuzione, se gli va proprio male.

6 mesi dopo, scopre che ha instalato tanta di quella roba su WSL, che gli pesa 45gb e ha bisogno di un esorcismo per compilare un pacchetto senza fallire. La VM? reinstallata altre 15 volte, con i soliti problemi di rete e la pesantezza che tutto un DE di linux può portare sul notebook di un consulente messo in croce.

Storia già vista.

Ma un giorno, il tuo nuovo Senior ti chiama e ti dice "Installa vagrant e lancialo con questo file di configurazione. Così puoi avere una macchina virtuale pronta per l'uso. La lanci e la fermi da riga di comando e uando non ti serve o non funziona o devi farci altro, la cancelli e ne crei una nuova."

In quel momento capisci di aver compreso il senso dell'universo, e pensi che vorresti ringraziare quei santi di Hashicorp uno ad uno, perché finalmente puoi fare __una__ configurazione, __una__ sola volta e riusarla all'infinito se qualcosa non va. E fai tutto dalla riga di comando. Animale strano che inizi ad apprezzare sempre di più.
La cosa figa è che con uno schiocco di dita è possibile (ram permettendo) tirare su un cluster di macchine virtuali. Già.

# Cos'è Vagrant

Vagrant è uno degli attrezzi nel vostro nuovo ~~coltellino svi~~ multiattrezzo.
Vagrant è la risposta ad una domanda che nessuno forse si era posto: come faccio ad avere un ambiente linux, pronto con quello che dico io, senza perderci tempo?

# Idempotenza

> In informatica, in matematica, e in particolare in algebra, l'idempotenza è una proprietà delle funzioni per la quale applicando molteplici volte una funzione data, il risultato ottenuto è uguale a quello derivante dall'applicazione della funzione un'unica volta.

Non importa quante volte voi eseguiate il provisioning di una vm con vagrant machine. Se scriverete bene il file di configurazione, otterrete sempre lo stesso risultato. L'idempotenza è uno dei principi cardine del DevOps.

# Pre-requisiti

* Vbox
* Vagrant
* editor a scelta (VScode)
* terminale a scelta (Windows Terminal)
* un ssd (lavorate pure su un disco meccanico, se avete voglia di soffrire)

Visto che sono particolarmente sfaticato, vi dico che a me piace usare Choco per gestire i miei robi di lavoro:

`choco install virtualbox vagrant editor-a-scelta`

Al lettore si rimanda la dimostrazione del teorema, l'installazione di Chocolatey, l'installazione dei pacchetti, la creazione di un nuovo repo git chiamato "ubuntu-cluster" ed un paio di riavvii del pc.

# Le basi, cazzo

`guest` è la macchina virtuale che create.
`host` è la macchina fisica, che ospiterà le vm.

[repository bento](https://vagrantcloud.com/bento)

qui trovate le immagini "bento" per le vostre vm. Le bento sono create da Chef (altra grossa corp che lavora in ambito DevOps/Operations), sono ottimizzate i nostri scopi di dev e sono opensource

> digressione: come vengono create le vm di vagrant? usando un altro programma di Hashicorp, Packer.

i comandi fondamentali:

`vagrant init ubuntu/focal64` inizializza un progetto per una vm ubuntu 20. crea un Vagrantfile con le direttive di base.

> I vagrantfile sono scritti in Ruby. Quindi sapete già che se volete diventare dei pro, vi tocca imparare quelle 4 direttive per farlo girare.

`vagrant up` crea una nuova vm a partire da un Vagrantfile. Oppure avvia la vm, se già esistente nel path in cui lanciate il comando

`vagrant ssh` apre una shell ssh nella vm. 

> Se la vostra vagrantbox condivide un'interfaccia di rete con il suo host, potrete eseguire direttamente un ssh vagrant@ip_guest, password: vagrant. Si risparmia qualche secondo di attesa, il risultato è il medesimo.

`vagrant halt` spegne la vm

# Comandi un po meno di base

`vagrant validate` "interpreta" il Vagrantfile e mostra eventuali errori.

`vagrant reload` esegue nuovamente il provision del guest. Non perderete dati, tranne quelli che verranno impattati direttamente dal file di provision scritto da voi.

`vagrant suspend` mette in sospensione la vm. Occuperà un po di preziosa memoria sul disco, ma manterrà lo stato delle vostre applicazioni.

`vagrant resume` "resuscita" dalla sospensione una vm.

`vagrant destroy -f` il potere di uccidere un guest ( -f ) e di eliminare qualsiasi sua traccia dal disco



# Playground, struttura

Ora creiamo una vm (o un cluster) con cui poter lavorare. Per Vagrant è sufficiente usare un init o un Vagrantfile per definire una vm. Quando le cose iniziano a richiedere un pelo di operazioni in più, è meglio disaccoppiare gli elementi.

il progetto richiederà poco lavoro

`boxes.yml` Il file che conterrà le conf. di base della/e vm
`provision.sh`  Uno script che verrà eseguito al primo avvio o al reload della/e vm
`Vagrantfile` Il file che Vagrant usa per lanciare il provision


# Boxes.yml

Definiamo una struttura dati arbitraria che contenga la configurazione delle nostre macchine. Vagrant, o per meglio dire Ruby, vi da la possibilità di leggere un qualsiasi tipo di file dal disco. Ovviamente sceglieremo uno Yaml, perché è facilmente leggibile dall'essere umano. Lo Yaml verrà interpretato in Ruby come una mappa.

```yaml
---
- hostname: worker1
  role: worker1
  box: bento/ubuntu-20.04
  box_version: 202008.16.0
  cpus: 4
  memory: 2048
  ip: 192.168.57.101
  provision: provision.sh
  shares:
    - host: C:/Users/ozeta/ubuntu-runner/share
      guest: /home/vagrant/share
    - host: C:/Users/ozeta/ubuntu-runner/work
      guest: /home/vagrant/work
  autostart: true
- hostname: worker2
  role: worker2
  box: bento/ubuntu-20.04
  box_version: 202008.16.0
  cpus: 4
  memory: 2048
  ip: 192.168.57.102
  provision: provision.sh
  shares:
    - host: C:/Users/ozeta/ubuntu-runner/share
      guest: /home/vagrant/share
    - host: C:/Users/ozeta/ubuntu-runner/work
      guest: /home/vagrant/work
  autostart: true
```

cosa succede?

Nella radice del documento ho dichiarato un array contenente 2 oggetti anonimi. Ogni oggetto corrisponde ad una vm che verrà creata sul nostro host. Per lo scopo di questo esercizio, le vm saranno identiche tranne che per l'indirizzo IP, ruolo e hostname. L'oggetto contiene i seguenti campi:

`hostname` è l'hostname che verrà assegnato al guest.
`role` è un attributo che potremo riutilizzare nel nostro Vagrantfile.
`box` è l'immagine che useremo per creare le nostre vm.
`box_version` identifica la versione dell'immagine.
`cpus` assegneremo ad ogni vm 4 cpu virtuali.
`memory` assegneremo 2 gb di ram ad ogni macchina. Il vostro host dovrà quindi avere 4gb liberi o, nel caso sia necessario, dovete ridurre le dimensioni assegnate.
`ip` assegneremo un ip alla macchina, a cui potremo connetterci
`provision` una volta che la vm sarà inizializzata, verrà eseguito questo script.
`shares` Vagrant configura automaticamente delle cartelle condivise tra host e guest. In questa sezione dichiaro un array, di dimensione arbitraria, di cartelle che voglio condividere tra gli ambienti.

# Provision.sh

Questa dimostrazione è una semplice POC, quindi ci basta qualcosa di facile: stampiamo a video una variabile recuperata da Boxes.yml ed installiamo Python3 e pip3

> Tutti i comandi vengono eseguiti come root, quindi non sarà necessario usare "sudo" prima dei comandi.

> Quando sarà lanciato il provisioner shell, __non__ avremo la possibilità di interagire tramite stdin con la nostra console. quindi dovremo fare molta attenzione a come eseguire i nostri comandi.

apt-get richiede la conferma di installazione dei pacchetti. l'opzione -y disabilita la conferma.


```bash
#!/bin/bash
echo "Guest VM IP: ${1}"
apt-get update
apt-get install -y python python3-pip # occhio al -y!

echo "Ciao!"
```


# Vagrantfile 

Siamo arrivati finalmente al cuore pulsante della nostra configurazione. Nel vagrantfile faremo una serie di operazioni di routine.

In particolare, verrà dichiarata una private_network. Alla vm guest assegnerò un indirizzo ip statico arbitrario (ad esempio 192.168.57.101) per poter accedere comodamente ai server che in futuro vorrò esporre sulle porte della vm. Verrà creata una interfaccia di rete virtuale.

Sarà possibile raggiungere la vm solo dal pc host, che sarà convinto che la macchina in questione sia un qualsiasi pc sulla rete. 

> Al contrario, per mettere su rete pubblica la vm, si utilizza una public_network. Usando una rete pubblica sarà necessario, durante il bootstrap della vm, scegliere l'interfaccia di rete fisica. Nel caso si scelga di usare un ip statico, stavolta questo dovrà essere coerente con la nostra configurazione di rete.

Lo ammetto, la sintassi di vagrant è da mal di testa. Ma funziona.

Tentando di essere chiaro prima di leggere il nostro Vagrantfile, questo è quello che ho intenzione di fare: 
1. Carico il file boxes.yml in una lista
2. Per ogni vm contenuta nella lista:

  2.1. Verrà creatà una vm usando le specifiche (host, cpu, ram) lette dal file.

  2.2. verrà creato una interfaccia di rete privata

  2.3. Per ogni cartella "shares", creo una synced_folder

  2.4. eseguo il file di provision, passandogli un array di 
argomenti (in questo caso contiene solo l'indirizzo ip)


```ruby
# -*- mode: ruby -*-
# vi: set ft=ruby :
#vagrant 2.2.10

require 'yaml'
boxes = YAML.load_file('./boxes.yml')

Vagrant.configure("2") do |config|
  boxes.each do |box|
    config.vm.define box['hostname'] do |host|
      host.vm.box = box['box']
      host.vm.box_version = box['box_version']
      host.vm.hostname = box['hostname']

      host.vm.provider "virtualbox" do |vb|
        vb.memory = box['memory']
        vb.cpus = box['cpus']
      end
      
      host.vm.network "private_network", ip: box['ip']
      
      box['shares'].each do |share|
        host.vm.synced_folder share['host'], share['guest'], type: 'virtualbox' , create: true
      end

      host.vm.provision "shell", path: box['provision'], :args => [box['ip']]
      
    end
  end
end
```

Tentiamo di capire che succede....

```ruby
require 'yaml' # importo il modulo yaml
boxes = YAML.load_file('./boxes.yml') # carico il file

Vagrant.configure("2") do |config| # creo un oggetto config con cui opererò i guest
  boxes.each do |box| ## ciclo nell'array dei box
    ...
  end
end
```

Fin qui tutto facile. Avvio il configuratore di Vagrant, che per motivi mistici è alla versione "2". Questo mi alloca un oggetto config, che verrà impiegato più tardi. Fermiamoci un istante per apprezzare la sintassi di Ruby.
Ciclo for sull'array, terzo giorno di scuola.


Entriamo nel dettaglio:

Adesso userò le api di vagrant per una configurazione [multi machine](https://www.vagrantup.com/docs/multi-machine) e per la configurazione di [virtual box](https://www.vagrantup.com/docs/providers/virtualbox/configuration)

A questo punto posso lavorare sulla singola macchina.
Configurazioni di base, poi rete, poi shared folders, ed infine il lancio del provisioner shell.

Vagrant mette a disposizione una vasta gamma di [provisioner](https://www.vagrantup.com/docs/provisioning/basic_usage)

```ruby
    config.vm.define box['hostname'] do |host| # definisco una nuova vm, a cui do come nome il nostro hostname
      host.vm.box = box['box'] # indico l'immagine da usare
      host.vm.box_version = box['box_version'] # la versione specifica
      host.vm.hostname = box['hostname'] # l'hostname

      host.vm.provider "virtualbox" do |vb| # qui assegno le risorse fisiche
        vb.memory = box['memory']
        vb.cpus = box['cpus']
      end
      
      host.vm.network "private_network", ip: box['ip'] # imposto il network privato, con ip statico
      
      box['shares'].each do |share| # ciclo sugli shares definiti nel box.yml
        host.vm.synced_folder share['host'], share['guest'], type: 'virtualbox' , create: true
      end

      host.vm.provision "shell", path: box['provision'], :args => [box['ip']] # lancio un provisioner shell, passo un array che contiene l'ip della macchina
      
    end
  end
```


A questo punto non ci resta che provare il tutto.

Un rapido check:

```powershell
PS C:\Users\ozeta\OneDrive\Desktop\ubuntu-runners> vagrant validate
==> vagrant: A new version of Vagrant is available: 2.2.13 (installed version: 2.2.10)!
==> vagrant: To upgrade visit: https://www.vagrantup.com/downloads.html
```

Lanciamo il provision.
Possiamo notare come vagrant identifichi la macchina su cui sta lavorando:

```powershell
PS C:\Users\ozeta\OneDrive\Desktop\ubuntu-runners> vagrant up
Bringing machine 'worker1' up with 'virtualbox' provider...
Bringing machine 'worker2' up with 'virtualbox' provider...
==> worker1: Importing base box 'bento/ubuntu-20.04'...
==> worker1: Matching MAC address for NAT networking...
==> worker1: Checking if box 'bento/ubuntu-20.04' version '202008.16.0' is up to date...
==> worker1: Setting the name of the VM: ubuntu-runners_worker1_1605136965674_39052
==> worker1: Clearing any previously set network interfaces...
==> worker1: Preparing network interfaces based on configuration...
    worker1: Adapter 1: nat
    worker1: Adapter 2: hostonly
==> worker1: Forwarding ports...
    worker1: 22 (guest) => 2222 (host) (adapter 1)
==> worker1: Running 'pre-boot' VM customizations...
==> worker1: Booting VM...
==> worker1: Waiting for machine to boot. This may take a few minutes...
```

Ad un certo punto sarà loggata l'attività del nostro provisioner shell:


```powershell
==> worker1: Running provisioner: shell...
    worker1: Running: C:/Users/ozeta/AppData/Local/Temp/vagrant-shell20201112-16244-140103d.sh
    worker1: Guest VM IP: 192.168.57.101
    worker1: Hit:1 http://archive.ubuntu.com/ubuntu focal InRelease
    worker1: Get:2 http://security.ubuntu.com/ubuntu focal-security InRelease [107 kB]
    worker1: Get:3 http://archive.ubuntu.com/ubuntu focal-updates InRelease [111 kB]
    worker1: Get:4 http://archive.ubuntu.com/ubuntu focal-backports InRelease [98.3 kB]
    worker1: Get:5 http://security.ubuntu.com/ubuntu focal-security/main i386 Packages [153 kB]
    worker1: Get:6 http://security.ubuntu.com/ubuntu focal-security/main amd64 Packages [367 kB]
 

...


    worker1: Ciao!

...

    worker2: Setting up gcc-9 (9.3.0-17ubuntu1~20.04) ...
    worker2: Setting up libpython3-dev:amd64 (3.8.2-0ubuntu2) ...
    worker2: Setting up libstdc++-9-dev:amd64 (9.3.0-17ubuntu1~20.04) ...
    worker2: Setting up gcc (4:9.3.0-1ubuntu2) ...
    worker2: Setting up python3-dev (3.8.2-0ubuntu2) ...
    worker2: Setting up g++-9 (9.3.0-17ubuntu1~20.04) ...
    worker2: Setting up g++ (4:9.3.0-1ubuntu2) ...
    worker2: update-alternatives: 
    worker2: using /usr/bin/g++ to provide /usr/bin/c++ (c++) in auto mode
    worker2: Setting up build-essential (12.8ubuntu1.1) ...
    worker2: Processing triggers for libc-bin (2.31-0ubuntu9) ...
    worker2: Processing triggers for man-db (2.9.1-1) ...
    worker2: Processing triggers for mime-support (3.64ubuntu1) ...
    worker2: Ciao!

```

# Risultato

![le nuove vm](https://i.imgur.com/T5firWq.png)

Adesso abbiamo creato 2 nuove vm, che possiamo pilotare sia nella maniera tradizionale, dal pannello di Vbox, che tramite i comandi di vagrant.

Lanciando `vagrant ssh`, vagrant non saprà su quale dei 2 nodi avviare la console. dovremo quindi aggiungere la destinazione:

`vagrant ssh worker1` 


```powershell
PS C:\Users\ozeta\OneDrive\Desktop\ubuntu-runners> vagrant ssh worker1
Welcome to Ubuntu 20.04.1 LTS (GNU/Linux 5.4.0-42-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Wed 11 Nov 2020 11:33:32 PM UTC

  System load:  0.0               Processes:             131
  Usage of /:   2.4% of 61.31GB   Users logged in:       0
  Memory usage: 8%                IPv4 address for eth0: 10.0.2.15
  Swap usage:   0%                IPv4 address for eth1: 192.168.57.101


121 updates can be installed immediately.
54 of these updates are security updates.
To see these additional updates run: apt list --upgradable



This system is built by the Bento project by Chef Software
More information can be found at https://github.com/chef/bento
vagrant@worker1:~$
```

Enjoy the ride, e alla prossima puntata 🤠