Distribuzione del software gestita: un possibile approccio

Introduzione: Evoluzione e Complessità delle Applicazioni e Infrastrutture

Nel corso degli ultimi anni si è assistito ad un progressivo ed esponenziale incremento della complessità sia delle applicazioni che delle infrastrutture. Questo da una parte ha contribuito all'incremento di performances, scalabilità e sicurezza, dall'altra ha tuttavia imposto un nuovo approccio al tema del deploy negli ambienti in cui andrà in esercizio la nostra applicazione.

È importante porre l'attenzione sul fatto che ad una crescente complessità del sistema (app, infrastrutture, network) è strettamente legato un crescente costo di realizzazione e, non meno trascurabile, di gestione.

Diventa quindi strategico individuare la giusta soluzione per evitare di incorrere in inutili costi aggiuntivi e al contempo permettere di garantire il mantenimento una situazione sicura e gestita.

Il deploy di un'applicazione può avvenire in contesti diversi: dalle soluzioni on-premise alle soluzioni SaaS, dalle soluzioni bare-metal alle soluzioni di tipo cluster (es. Kubernetes).

Considerando il ventaglio di tecnologie in ambito di sviluppo e quello delle tecnologie in ambito infrastrutturale otteniamo una quantità di soluzioni estremamente ampia. Questo ci spinge ad accentuare il focus sulla individuazione delle soluzioni da adottare fin dalla partenza del nostro progetto, anche avvalendosi di un supporto specialistico, per non incorrere in spiacevoli sorprese dopo che il nostro sistema è in esercizio.

Deploy su Macchine Virtuali (VM)

Una soluzione tecnologica di buon equilibrio tra complessità, costi e prestazioni è quella di effettuare il deploy su VM (Virtual Machine), la soluzione, come altre, è adottabile sia in un contesto on-prem che in un contesto di tipo cloud, demandando la gestione della soluzione nel primo caso all'interno dell'organizzazione oppure ad un partner tecnologico di fiducia, nel secondo caso in capo al cloud provider.

Senza entrare troppo nei dettagli, trattandosi di una soluzione che eroga un OS(Sistema Operativo) attraverso un HW virtualizzato, poggiato su uno o più nodi fisici (Bare Metal), a tutti gli effetti sarà possibile disporre di una macchina che a seconda dei casi, avrà possibilità di accesso e utilizzo del tutto simili ad una macchina fisica. Questo tipo di soluzione fornirà agevolmente accesso di tipo SSH oppure SFTP.

Deploy su Macchine Virtuali (VM)

Si potrebbe essere tentati di utilizzare la VM stessa come ambiente di sviluppo o si potrebbe essere addirittura tentati di applicare hotfix e upgrade direttamente nella macchina di produzione.
Se da una parte potrebbe sembrare una ottima scorciatoia in termini di tempo, evitando "tout court" di affrontare il setup di un sistema di deployment, qualsiasi esso sia, dall'altra garantisce in breve tempo una completa perdita di controllo rispetto al SW in esercizio.

Approccio Version Control Driven

Avviciniamoci per passi al sistema che andremo ad analizzare, il primo principio a cui si ispira è quello denominato Version Control Driven, ovvero ogni tipo di aggiornamento al codice avviene attraverso un sistema di versionamento del codice, in questo caso, come ampiamente diffuso, attraverso Git.

Git è un sistema di controllo versione distribuito per tracciare modifiche nel codice sorgente durante lo sviluppo software.
Utilizzare Git per apportare le modifiche al codice sorgente della nostra applicazione sulla VM è già un primo passo per acquisire un certo controllo sul ciclo di vita del SW.

Deployer

Le soluzioni fin qui applicate offrono qualcosa di totalmente manuale, che non tiene conto della compilazione di un eventuale artefatto, funzionante con linguaggi non compilati (es PHP o Python), ma rappresentano pur sempre un notevole miglioramento rispetto ad un deployment eseguito via FTP.

Come esposto all'inizio la complessità delle applicazioni è sempre crescente, e ci troviamo spesso a confrontarci con framework che utilizzano sistemi di gestione delle librerie e delle dipendenze tra di esse, come Composer per PHP o PIP per Python.

Tralasciando la pessima idea di copiarle fisicamente, resta la necessità di dover eseguire sulla macchina quei comandi che servono per allineare il codice delle librerie a quanto dichiarato nei file (già versionati) con le specifiche dei pacchetti stessi.
Il tool specifico di gestione pacchetti (Composer o PIP) scaricherà e copierà i pacchetti per voi.

Possiamo parlare di un sistema correttamente gestito, non possiamo tuttavia parlare di un sistema "smart"; una prassi che possa vantare una qualsiasi forma di automatismo: ogni cosa deve essere gestita a mano, la sequenza e i parametri di esecuzione sono affidati a chi opera in quel preciso momento. Cominciamo quindi a capire che esiste anche un problema di replicabilità del processo.

È a questo punto che entra in gioco il sistema di deployment.

Negli esempi successivi faremo riferimento al framework deployer, essendo PHP il nostro linguaggio di riferimento; esistono molte valide alternative (come ad esempio Capistrano, scritto in Ruby).

Ipotizziamo il deploy di una applicazione attraverso deployer su una VM, più precisamente immaginiamo di effettuare il deploy di un'applicazione basata su Drupal 10, framework per la gestione dei contenuti (CMS) di una certa complessità.

Il primissimo passo da compiere è installare deployer.

Dove installarlo dipenderà dalle necessità operative e di cooperazione rispetto alla distribuzione del nostro applicativo e all'organizzazione del team (chi segue questo tema? chi si occupa di DevOps è un gruppo indipendente? devono agire attori esterni all'organizzazione? si opera su singolo host? domande di questo tipo dovrebbero aiutare nella scelta).

Deployer è un tipico software PHP che sfrutta un framework, installabile sfruttando il Package Manager Composer.

Installazione:
composer require --dev deployer/deployer

Inizializzazione:

vendor/bin/dep init

Per qualsiasi approfondimento: https://deployer.org/docs/7.x/installation

Una volta installato con successo, procediamo a creare la nostra prima ricetta, infatti così vengono nominati i singoli progetti (recipe). Anche in questo caso come organizzare all'interno dell'applicazione i vari progetti di deploy e come eventualmente organizzare iterazioni/relazioni tra di essi fa parte delle strategie che ogni singola realtà vorrà perseguire.

Un interessante elemento a favore di questo framework è che oltre a poter gestire task relativi al deployment può essere utilizzato per creare task custom ed eseguirli negli host dichiarati in un determinato progetto.

Nella nostra esperienza ad esempio è stato creato un task custom per effettuare il download dei vari DB presenti sugli host relativi ad un determinato progetto; molto utile (e comodo) per allineare in termini di dati contesti di sviluppo locale.

Una volta eseguito il deploy della nostra applicazione, nel server potremo osservare una struttura di questo tipo:

~/example // The deploy_path.
        |- current -> releases/1 // Symlink to the current release.
        |- releases // Dir for all releases.
        |- 1 // Actual files location.
        |- ...
        |- .env -> shared/.env // Symlink to shared .env file.
        |- shared // Dirs for shared files between releases.
        |- ...
        |- .env // Example: shared .env file.
        |- .dep // Deployer configuration files.

Descriviamo la struttura:

  • tutto quello che è codice custom è depositato nella cartella releases dentro una sub folder che rappresenta il numero di release (EG 1), fino al massimo di release dichiarate a livello di progetto.
    Quando si supererà eventualmente il numero dichiarato, la release più vecchia verrà eliminata
  • la release corrente, ovvero quella che verrà utilizzata, è un link simbolico all'ultima disponibile, ma può essere modificata puntando una release specifica in caso di necessità.
  • la folder shared a sua volta conterrà tutte le componenti statiche, librerie, ambiente, configurazioni, che saranno collegati sempre tramite link simbolici alla release corrente.
  • la folder .dep contiene informazioni specifiche del deployment del progetto ad esempio tiene traccia delle date di caricamento delle release, e contiene i dati del sistema di Version Control (Git).

Questo potrebbe essere sufficiente per un'applicazione HTML statica, oppure una applicazione PHP priva di framework, ma non basta per Drupal 10, il nostro caso d'uso.
Infatti questo CMS ogni qualvolta venga "deploiato" necessita l'esecuzione di una serie di comandi a seguito dell'allineamento del codice tramite Version Control, riportiamo di seguito la lista di esecuzione dei task:

task('deploy', [
    'deploy:prepare',
    'deploy:generate_settings',
    'deploy:vendors',
    'deploy:publish',
    'deploy:compile_scss',
    'deploy:drupal_drush'
]);

Come si può notare una serie di sub task evidenziati in verde, sono completamente custom e verranno eseguiti nella sequenza stabilita dal task principale denominato "deploy".

Di seguito una breve descrizione di ogni singolo sub-task:

  • deploy:prepare recupera il branch indicato dal repo Git stabilito.
  • deploy:generate_settings ricostruisce i file di configurazioni dell'applicazione a partire dalle variabili di envelope specifiche per l'ambiente.
  • deploy:vendors esegue i comandi Composer per allineare le librerie a quanto dichiarato nei file di configurazione (versionati).
  • deploy:compile_scss esegue la compilazione tramite Yarn dei file SCSS per ottenere un output CSS.
  • deploy:drupal_drush esegue una serie di comandi CLI di Drupal.

Senza i task custom evidenziati, l'applicazione non funzionerebbe correttamente.

Ad esempio senza il task che ricostruisce i settings, l'unica alternativa sarebbe quella di costruirli o caricarli manualmente.

Senza la compilazione dei file SCSS il sistema non sarebbe in grado di rendere una qual si voglia modifica al tema.

Senza l'esecuzione dei comandi drush, Drupal non sarebbe in grado di importare le configurazioni "versionate" che vengono dichiarate sotto forma di file yaml, non eseguirebbe in automatico la pulizia della cache, indispensabile dopo eventuali aggiornamenti di librerie, e non eseguirebbe un dump del DB prima di qualsiasi modifica, indispensabile per un efficace rollback.

Riportiamo, partendo da un progetto reale, generalizzando alcuni parametri specifici, il codice sorgente del task di compilazione SCSS.

task('deploy:compile_scss', function (){
    $conf = get('project_conf');
    writeln("<info>Execute SCSS compiling (YARN)</info>");
    $base_dir = $conf['hosts']['myhost'][$conf['hosts']['myhost']['env'].'_deploy_dir'].'/current/web/themes/custom/mycustomtheme/dist';
    $r = run('rm -rf '.$base_dir.'/*');
    writeln($r);
    $base_dir = $conf['hosts']['myhost'][$conf['hosts']['myhost']['env'].'_deploy_dir'].'/current/web/themes/custom/mycustomtheme/source';
    cd($base_dir);
    $r = run('yarn install && yarn run prod', [], null, null, null, null, true, false);
    writeln($r);
    $base_dir = $conf['hosts']['myhost'][$conf['hosts']['myhost']['env'].'_deploy_dir'].'/current';
    cd($base_dir);
    $r = run('vendor/bin/drush cr', [], null, null, null, null, true, false);
    writeln($r);
});

Per ogni comando eseguito ne viene riportato l'output tramite la funzione writeln(), il comando principale che viene eseguito sulla VM tramite la funzione run() di deployer è "yarn install && yarn run prod" che esegue effettivamente la compilazione SCSS.

L'applicazione yarn non è disponibile di default in una tipica installazione di un OS Linux, ma è facilmente installabile.

È possibile creare una definizione di una certa configurazione per la VM, che preveda anche l'installazione di yarn, oltre l'installazione di un preciso stack LAMP, definizione che verrà successivamente tradotta nell'effettivo stato della VM; questo però è un tema diverso, si tratta di IaC (Infrastructure As Code), e certamente merita uno spazio ad hoc per essere trattato.

Conclusione

Concludendo, riteniamo che questo strumento abbia tutti i requisiti per una corretta ed efficace gestione del deployment:

  • Caricamento gestito a partire dal codice sorgente "versionato"
  • Presenza di un processo di deploy, e in caso di necessità di rollback.
  • Possibilità di eseguire una lista di operazioni nella voluta sequenza e con le volute parametrizzazioni.
  • Possibilità di creare task custom oltre a quello di "deploy"