Drupal + Deployer = Love

Drupal + Deployer = Love

Martin Adams, Unsplash

Automatizzare la “distribuzione” (deploy) di un progetto software

Una delle esigenze di chi sviluppa software web based è quella di mettere in linea il software (normalmente un sito web) e magari farlo in diversi ambienti. Ad esempio con la classica metodologia che vede un ambiente di test separato da quello di produzione (e ovviamente il tutto separato dagli ambienti di sviluppo dei programmatori). Mentre nei primi anni del web si utilizzava molto spesso il protocollo FTP per caricare a mano i progetti, via via si è preferito studiare automatismi, in particolare per tre motivi:

  • Ridurre i tempi di caricamento.
  • Limitare i potenziali errori causati da una attività manuale.
  • Inoltre l’automazione del processo di deploy permette di introdurre test che verificano il funzionamento del sistema per limitare ulteriormente i problemi che potrebbero nascere in seguito ad un aggiornamento.

Sono quindi nati numerosi strumenti e metodologie che hanno portato ai pattern denominati CI/CD, ovvero Continuous Integration e Continuous Deployment.

Gli strumenti per l’automazione del processo di deploy

Nel corso del tempo sono nati numerosi strumenti, anche piuttosto complessi, i più famosi sono:

  • Capistrano
  • Jenkins
  • CircleCI
  • GitLab
  • Travis

E poi abbiamo strumenti un po’ più semplici, ma comunque utili in molte situazioni, soprattutto per progetti di piccole dimensioni o comunque quando non servono livelli di automazione molto spinta. Tra questi segnalo:

  • Mina
  • Deployer

Il primo (Mina) è uno strumento nato in ambiente Ruby on Rails, il secondo (Deployer) è molto simile a Mina, ma sviluppato in PHP. Ed è proprio con Deployer che in questo articolo vedremo come gestire il deploy di un sito in Drupal 8/9.

(Molto) Breve introduzione a PHP Deployer

PHP Deployer è uno script scritto in PHP che si occupa di gestire la pubblicazione di un progetto e permette di mantenere uno storico delle ultime versioni nel server per effettuare un eventuale rollback di emergenza riducendo al massimo i tempi morti. Nel server la struttura del progetto verrà creata dallo script in questo modo:

  • releases: directory con dentro le ultime n release (con n impostato nella configurazione dello script)
  • releases/[release_number]: ogni release ha un numero progressivo che la identifica
  • current: link simbolico verso l’ultima release

Il web server dovrà quindi puntare la root del sito alla directory current (o ad una sotto directory in base al tipo di progetto) per mostrare sempre l’ultima release. Deployer si occupa in autonomia di effettuare tutta la gestione delle varie release e, con un facile sistema di configurazione permette allo sviluppatore di personalizzare le varie fasi del deploy anche su più ambienti diversi.

Personalizzare la ricetta di default

Deployer ha già delle cosiddette “ricette” (recipes) pronte per i vari CMS, compreso Drupal 8 (che va bene anche per Drupal 9), però non tiene conto che spesso (anche se non è elegante) si utilizzano Composer ed NPM per l’installazione delle dipendenze direttamente sul server in modo da tenere pulito il repository del codice sorgente del progetto. Per ridurre il downtime del sito, tali pacchetti aggiuntivi verranno installati PRIMA di effettuare lo switch della nuova release su current.

In questo articolo ipotizzo che Deployer sia già stato installato e che il progetto sia già stato inizializzato per Deployer (beh quest’ultima fase è banale, in quanto basta eseguire il comando dep init dalla directory principale del progetto ed il gioco è fatto). Per maggiori dettagli su come installare e vedere le basi di configurazione rimando alla guida ufficiale, non è scritta benissimo, ma per partire va bene lo stesso: Getting Started .

Nell’esempio che farò ipotizzo di avere un ambiente di sviluppo locale, un server di test ed un server di produzione. Inoltre ricordo che il tema Drupal che sto usando come esempio ha delle dipendenze gestite tramite NPM.

Deployer, per funzionare, ha bisogno di un file chiamato deploy.php nella directory principale del progetto. In questo caso la configurazione di Deployer si baserà sulla ricetta ufficiale per Drupal 8, ma con alcune piccole integrazioni.

Installazione delle dipendenze con Composer

Per prima cosa è necessario impostare le opzioni di composer nel seguente modo:

set('composer_action', 'install');
set('composer_options', '{{composer_action}} --verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader --no-suggest');

Dopodiché è necessario agganciarsi alla sequenza di task (ovvero le operazioni di deploy definite dalla ricetta di default per Drupal 8) attivando composer prima che la nuova release venga linkata su current. Per farlo si usa il codice seguente:

before('deploy:symlink', 'deploy:vendors');

Dove deploy:vendors è il task standard di Deployer che richiama Composer, task che non viene contemplato dalla ricetta per Drupal 8.

Installazione di librerie con NPM per il tema Drupal

Mentre per Composer esiste già un task di default, per NPM è necessario creare un nuovo task personalizzato che dipenderà dallo specifico progetto. Nel mio caso ho un template custom chiamato exampletheme all’interno del quale va richiamato il comando npm install --only=production. Chiaramente ciò necessita che NPM sia installato nel server di destinazione.

Per definire il task si usa questo codice:

task('deploy:npm_install', function () {
    run('cd {{release_path}}/web/themes/custom/exampletheme && npm install --only=production');
})->desc('running npm install');

Una volta definito il task, come per Composer, bisogna eseguirlo prima del task deploy:symlin, in questo modo:

before('deploy:symlink', 'deploy:npm_install');

Bonus 1: backup del database

Deployer normalmente non effettua il backup del database ma tiene traccia solo dei file di progetto, arricchiamo quindi il tutto con un task dedicato al backup del database:

task('deploy:drupal_db_dump', function () {
    $backup_timestamp = date("Y-m-d-h-m-s", time());
    run('cd {{release_path}} && drupal dbdu --gz --file={{application}}_{{stage}}_'.$backup_timestamp.'.sql.gz && mv web/{{application}}_{{stage}}_'.$backup_timestamp.'.sql.gz ~/db_backups/ ');
})->desc('running DB backup using drupal console');

Questo task sfrutta Drupal Console per effettuare il backup nella directory ~/db_backups/ e viene eseguito all’inizio della procedura.

Bonus 2: aggiornamento DB e pulizia cache

Infine, dopo che il deploy è stato effettuato va fatto l’aggiornamento del DB e la pulizia delle cache di Drupal usando lo script Drush:

// - Aggiornamento database
task('deploy:drupal_update_database', function () {
    run('cd {{release_path}}/web && drush updb --yes');
})->desc('running drupal database update');

//  - Pulizia cache di Drupal
task('deploy:drupal_cache_rebuild', function () {
    run('cd {{release_path}}/web && drush cr');
})->desc('running drupal cache rebuild');

La ricetta completa di Deployer per Drupal 8 e per Drupal 9 con Composer e NPM

Ecco quindi la ricetta completa per il deploy di un sito web sviluppato con Drupal 8 o con Drupal 9 che comprende l’installazione delle dipendenze con Composer e delle librerie necessarie al tema con NPM.

Ovviamente il codice ha delle stringhe di esempio che andranno poi modificate progetto per progetto.

<?php
namespace Deployer;
require 'recipe/drupal8.php';

/**
 * Setup
 */

// Nome del progetto (es: example.com)
set('application', 'NOME_PROGETTO');

// Percorso del repository git nella forma "ssh://nome@dominio:porta/percorso"
set('repository', 'ssh://USER_NAME@DOMAIN:PORT/PERCORSO_REPOSITORY_GIT');

// Attiva una console tty per far funzionare il comando 'git clone'
set('git_tty', true);

// Percorsi dei file condivisi tra le release
set('shared_files', [
    'web/sites/{{drupal_site}}/settings.php',
    'web/sites/{{drupal_site}}/services.yml',
]);

// Percorsi delle directory condivise tra le release
set('shared_dirs', [
    'web/sites/{{drupal_site}}/files',
    'web/wp-content',
    'web/pdf'
]);

// Directory scrivibili dal webserver
set('writable_dirs', ['web/sites/{{drupal_site}}/files']);
set('allow_anonymous_stats', false);

// Host (staging e produzione)
// - Dati accesso per server di staging, la destinazione nel mio caso è staging.NOME_PROGETTO
host('INDIRIZZO_SERVER_STAGING')
    ->stage('staging')
    ->user('USER_NAME')
    ->port(22)
    ->set('deploy_path', '~/vhosts/staging.{{application}}');
// - Dati accesso per server di produzione, la destinazione nel mio caso è www.NOME_PROGETTO
host('INDIRIZZO_SERVER_PRODUZIONE')
    ->stage('production')
    ->user('USER_NAME')
    ->port(22)
    ->set('deploy_path', '~/vhosts/www.{{application}}');

// Opzioni per Composer
set('composer_action', 'install');
set('composer_options', '{{composer_action}} --verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader --no-suggest');


/**
 * Task Personalizzati
 */

// - NPM install
task('deploy:npm_install', function () {
    run('cd {{release_path}}/web/themes/custom/zero && npm install --only=production');
})->desc('running npm install');

// - Backup del database
task('deploy:drupal_db_dump', function () {
    $backup_timestamp = date("Y-m-d-h-m-s", time());
    run('cd {{release_path}} && drupal dbdu --gz --file={{application}}_{{stage}}_'.$backup_timestamp.'.sql.gz && mv web/{{application}}_{{stage}}_'.$backup_timestamp.'.sql.gz ~/db_backups/ ');
})->desc('running DB backup using drupal console');

// - Aggiornamento database
task('deploy:drupal_update_database', function () {
    run('cd {{release_path}}/web && drush updb --yes');
})->desc('running drupal database update');

//  - Pulizia cache di Drupal
task('deploy:drupal_cache_rebuild', function () {
    run('cd {{release_path}}/web && drush cr');
})->desc('running drupal cache rebuild');

// Se il defloy fallisce effettua l'unlock
after('deploy:failed', 'deploy:unlock');

// Modifico la ricetta di Drupal 8 per fare il backup del DB
before('deploy:info', 'deploy:drupal_db_dump');

// Modifico la ricetta di Drupal 8 facendo in modo che  prima di collegare
// "current" alla nuova release vengano installate le dipendenze con composer...
before('deploy:symlink', 'deploy:vendors');
// ... e con NPM
before('deploy:symlink', 'deploy:npm_install');

// Modofico la ricetta di Drupal 8 con l'update del DB e la pulizia delle cache
// alla fine di tutto
after('deploy:cleanup', 'deploy:drupal_cache_rebuild');
after('deploy:cleanup', 'deploy:drupal_update_database');

// Imposta il numero di versioni da mantenere
set('keep_releases', 10);

Nota per la gestione dei permessi con Deployer

Attenzione: per far funzionare correttamente la gestione dei permessi con Deployer è necessario che nel server di destinazione ci sia sudo in modalità passwordless (cosa che sconsiglio) oppure sia installato il pacchetto acl. Nel caso di un server Debian prima di usare Deployer basta eseguire:

apt install acl
Leonardo Finetti

Leonardo Finetti
Si occupa di informatica dalla metà degli anni novanta principalmente in ambito web con tecnologie Open Source. Esperto di Drupal e di SEO offre consulenze in tali ambiti e nel tempo libero si diletta scrivendo articoli di informatica ed anche di design, ergonomia, usabilità e sicurezza.

Se ti piace questo sito puoi usare il link di affiliazione Amazon cliccando qui.