Blog

Si fa presto a dire “deploy” !

Lettura 8 minuti

Questo articolo nasce per raccontare la nostra esperienza con il tool Envoyer (https://envoyer.io/), utilizzato recentemente in Bitbull per effettuare il deploy di alcuni progetti Magento.

Andrò a descrivere in particolare l’approccio a questo tool e la procedura che abbiamo seguito per il deploy di un progetto Magento1.

Definizione… per non addetti ai lavori

Con il termine “deployment” si indica l’azione di consegna e messa in opera di una nuova versione di un’applicazione, che potrebbe anche essere il primo rilascio al cliente.

La definizione è abbastanza semplice ed immediata; la procedura che sta dietro ad un deploy non lo è altrettanto e ammetto che crea sempre una certa agitazione tra noi sviluppatori.

Bisogna infatti tenere conto principalmente del tempo di downtime dell’applicazione web, per assicurare il minor disservizio possibile per l’utente finale, e della sequenza in cui eseguire le operazioni sul server.

La procedura, se effettuata manualmente, è abbastanza ripetitiva e facilmente soggetta ad errori umani: da qui la scelta di affidare le operazioni ad un tool automatico.

Il tool

Envoyer è un tool online per automatizzare il deploy di applicazioni PHP su server singoli o multipli; si integra con piattaforme come GitHub, GitLab e Bitbucket per l’accesso ai repository git del progetto, con Composer per la gestione delle dipendenze applicative e con alcuni sistemi di chat come HipChat e Slack per l’invio di notifiche.

 

Tra le caratteristiche a mio avviso più interessanti ci sono:

  • possibilità di gestire le configurazioni d’ambiente sui singoli server
  • possibilità di configurare task aggiuntivi nel processo di deploy agganciandosi, tramite hook, alle operazioni previste dal flusso di esecuzione del tool
  • possibilità di impostare health-check sull’applicazione
  • Attualmente parte da un piano base da 10$ al mese per gestire fino a 10 progetti; si possono aggiungere collaboratori sui singoli progetti che potranno eseguire la maggior parte delle operazioni a fronte di una registrazione gratuita.

    Per una panoramica delle funzionalità di Envoyer rimando alla documentazione ufficiale.

    Configurazione di Envoyer

    Con Envoyer un progetto corrisponde ad un ambiente di erogazione dell’applicazione, distribuito su una o più macchine. Potremmo avere ad esempio un progetto per il deploy sull’ambiente di staging e uno per il deploy in produzione.

    Una volta creato il progetto, dal tab server, è necessario aggiungere la configurazione delle macchine su cui deployare specificando, per ognuna di esse, i parametri ssh di accesso, la cartella di progetto e i path degli eseguibili Php e Composer.

    Envoyer genera automaticamente una chiave ssh per ogni macchina, questa chiave deve essere aggiunta alle authorized key del server per consentire l’accesso da parte del tool in fase di deploy.

    Tramite la funzionalità manage environments è possibile, a livello di progetto, specificare una lista di variabili che possono essere sincronizzate sui singoli server in un file chiamato .env e posizionato nella cartella di progetto.

    Integrando nel progetto la libreria DotEnv (https://github.com/vlucas/phpdotenv) da uno script php si potrà accedere a queste variabili, esposte negli array $_ENV e $_SERVER.

    E’ possibile specificare anche la configurazione per le notifiche sull’esito dei deploy, dall’apposito tab, collegando ad esempio una stanza di HipChat o Slack.

    Quando parte un deploy Envoyer di default esegue, in sequenza, le seguenti operazioni:

  • clone della nuova release
  • installazione delle dipendenze composer
  • attivazione della nuova release
  • purge di vecchie release con una retention di 4 versioni
  • Queste operazioni sono elencate nel tab relativo ai deployment hooks che è il punto in cui Envoyer consente di configurare dei task supplementari da eseguire prima e/o dopo le singole operazioni di deploy, rendendo molto flessibile la procedura.

    Il dettaglio di ogni deployment è disponibile nel tab deployments (cliccando sulla freccina nella riga di dettaglio) e permette, per ogni operazione effettuata dal tool, di analizzare i comandi eseguiti sul server ed il loro esito.

    Deploy di progetti Magento

    Cosa succede sui server di pubblicazione?

    Per far partire un deploy ci posizioniamo nel tab deployments del progetto e premiamo il pulsante rosso posto in alto a destra (Deploy).

    Prima di iniziare la procedura verrà mostrata una finestra in cui specificare se scaricare la release dal default branch configurato nel progetto oppure da un branch o tag specifici.

    Grazie al collegamento tra il progetto ed il repository git di riferimento, Envoyer mostrerà la lista di branch/tag disponibili alla selezione del relativo radio button.

    Una volta che Envoyer termina le operazioni di download della release specificata e di installazione delle dipendenze tramite Composer, troveremo la nuova release all’interno della cartella releases, creata da Envoyer nel path di progetto.

    In fase di attivazione della nuova release Envoyer crea, sempre nel path di progetto, una directory current che è un symlink della release appena deployata. Questa dovrà corrispondere alla document root dell’applicazione.

    Il deploy di un progetto Magento (parliamo di Magento 1) richiede che alcune operazioni vengano eseguite prima  dell’attivazione della nuova release:

  • gestione file local.xml, differente per ogni ambiente (dev, staging, produzione, …)
  • esecuzione di script di upgrade della struttura dati
  • reindex Magento
  • flush della cache Magento
  • gestione risorse SEO statiche (robots.txt, sitemap.xml)
  • gestione cartella media di Magento, che non deve essere sovrascritta
  • gestione cartella var di Magento, che non deve essere sovrascritta
  • La nostra soluzione

    Per gestire le operazioni pre-attivazione della nuova release abbiamo agito su due fronti.

    Il primo creando una libreria “Bitbull/Deploy” che espone alcuni metodi da lanciare come script di post-install e post-update Composer e che viene installata come dipendenza da Composer.

    Di seguito la porzione “scripts” del composer.json del progetto:

    "scripts": {
        "post-update-cmd": [
          "Bitbull\\Deploy\\Magento::generateLocalXml",
          "Bitbull\\Deploy\\Magento::setMaintenanceMode",
          "Bitbull\\Deploy\\Magento::flushCache",
          "Bitbull\\Deploy\\Magento::runUpgradeScripts",
          "Bitbull\\Deploy\\Magento::reindexAll",
          "Bitbull\\Deploy\\Magento::unsetMaintenanceMode"
        ],
        "post-install-cmd": [
          "Bitbull\\Deploy\\Magento::generateLocalXml",
          "Bitbull\\Deploy\\Magento::setMaintenanceMode",
          "Bitbull\\Deploy\\Magento::flushCache",
          "Bitbull\\Deploy\\Magento::runUpgradeScripts",
          "Bitbull\\Deploy\\Magento::reindexAll",
          "Bitbull\\Deploy\\Magento::unsetMaintenanceMode"
        ]
      }
    

    Il primo metodo si occupa di generare il local.xml leggendo le variabili dal file .env proveniente da Envoyer. Per leggere tali variabili abbiamo installato come dipendenza composer la libreria DotEnv, già citata sopra.

    Gli altri metodi si occupano di eseguire le operazioni upgrade, reindex, flush cache Magento mediante l’esecuzione di comandi di n98-magerun, installato nel progetto sempre come dipendenza Composer (https://github.com/netz98/n98-magerun).

    namespace Bitbull\Deploy;
    use Composer\Script\Event;
    
    class Magento
    {
        // parametri definiti in Envoyer - Manage Environment e salvati in .env
        const APP_KEY         = "APP_KEY";
        const DB_HOST         = "DB_HOSTNAME";
        const DB_USERNAME     = "DB_USERNAME";
        const DB_PASSWORD     = "DB_PASSWORD";
        const DB_NAME         = "DB_NAME";
        const REDIS_ENDPOINT  = "REDIS_ENDPOINT";
        const SESSION_SAVE    = "SESSION_SAVE";
        const PSR0_NAMESPACES = "PSR0_NAMESPACES";
        const ADMIN_FRONTNAME = "ADMIN_FRONTNAME";
    
        public static function flushCache(Event $event)
        {
            $vendorPath = $event->getComposer()->getConfig()->get('vendor-dir');
            $binPath = $vendorPath . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR;
            exec($binPath . 'n98-magerun cache:flush', $output);
        }
    
        public static function runUpgradeScripts(Event $event)
        {
            $vendorPath = $event->getComposer()->getConfig()->get('vendor-dir');
            $binPath = $vendorPath . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR;
            exec($binPath . 'n98-magerun sys:setup:run', $output);
        }
    
        public static function reindexAll(Event $event)
        {
            $vendorPath = $event->getComposer()->getConfig()->get('vendor-dir');
            $binPath = $vendorPath . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR;
            exec($binPath . 'n98-magerun index:reindex:all', $output);
        }
    
        public static function generateLocalXml(Event $event)
        {
            $vendorPath = $event->getComposer()->getConfig()->get('vendor-dir');
            $extra = $event->getComposer()->getPackage()->getExtra();
    
            $magentoRootDir = realpath(dirname($vendorPath) . '/' . $extra['magento-root-dir']);
            $projectPath = dirname($magentoRootDir);
    
            if (self::existsLocalConf($magentoRootDir) && !self::overwriteConf()) {
                $event->getIO()->write("Local conf already exists, i will skip it!");
                return;
            }
    
            try {
                $dotenv = new \Dotenv\Dotenv($projectPath);
                $dotenv->load();
            } catch(\Exception $e) {
                $dotenv = new \Dotenv\Dotenv($magentoRootDir);
                $dotenv->load();
            }
    
            // parametri d'ambiente disponibili in $_SERVER per la generazione del local.xml
            // $_SERVER[self::APP_KEY]
            // $_SERVER[self::DB_HOST]
            // $_SERVER[self::DB_USERNAME]
            // $_SERVER[self::DB_PASSWORD]
            // $_SERVER[self::DB_NAME]
            // $_SERVER[self::SESSION_SAVE]
    
            ..........
        }
    }
    

    Infine, per la gestione delle risorse statiche abbiamo creato un hook da far eseguire ad Envoyer dopo l’operazione di installazione delle dipendenze composer. Si tratta di specificare una lista di comandi da eseguire sul server, con possibilità di utilizzare alcune variabili messe a disposizione da Envoyer e relative al progetto corrente ( {{project}} ) e al deployment in corso ( {{release}} ).

    La cartella storage, allo stesso livello di releases e current, è stata creata da noi manualmente sul server e contiene tutte le risorse che devono rimanere inalterate tra diversi deploy, per mantenere lo storico dei dati. Portandole fuori dalle release ci assicuriamo la persistenza di questi dati.

    Delegare questa operazione ad Envoyer, invece che alla libreria Bitbull/Deploy richiamata al termine del composer install, ci ha permesso di personalizzare i comandi per ogni ambiente di deploy (ad esempio per il progetto Envoyer relativo all’ambiente di staging non creiamo il symlink al file robots.txt).

    Da notare che Envoyer mette a disposizione uno strumento nativo per la creazione di symlink in fase di deployment, accessibile dal pulsante manage linked folders nel tab deployment hooks: purtroppo questa gestione dei symlink viene eseguita subito dopo l’operazione di download della nuova release e quindi, nel caso di progetti Bitbull, troppo in anticipo considerando che il core Magento viene installato come dipendenza Composer.

    Conclusioni

    Affidarsi ad un tool automatico per il deploy è un metodo sicuro e nel caso di Envoyer anche versatile, grazie alla possibilità di intervenire nel flusso delle operazioni di deploy.

    Una buona progettazione della soluzione permette di mantenere nel tool tutti i comandi specifici per ogni ambiente e, a livello di script, quelli comuni (con la gestione di eventuali variabili d’ambiente tramite il file .env).

    Inoltre, una volta configurato il progetto su Envoyer, anche figure non dev-ops possono effettuare dei deploy in quanto non è richiesto l’accesso al server ed è comunque possibile gestire un rollback ad una versione precedente.

    Prossimamente completeremo questo “tutorial” con delle indicazioni per progetti Magento 2 e con integrazione degli health-checks dell’applicazione.

    Nadia Sala