Distribuzione dei tempi di inattività di Laravel Zero
Pubblicato: 2022-03-11Quando si tratta di aggiornare un'applicazione live, ci sono due modi fondamentalmente diversi per farlo.
Nel primo approccio, apportiamo modifiche incrementali allo stato del nostro sistema. Ad esempio, aggiorniamo file, modifichiamo le proprietà dell'ambiente, installiamo necessità aggiuntive e così via. Nel secondo approccio, demoliamo intere macchine e ricostruiamo il sistema con nuove immagini e configurazioni dichiarative (ad esempio, utilizzando Kubernetes).
Distribuzione di Laravel semplificata
Questo articolo copre principalmente applicazioni relativamente piccole, che potrebbero non essere ospitate nel cloud, anche se menzionerò come Kubernetes può aiutarci notevolmente con le implementazioni oltre lo scenario "senza cloud". Discuteremo anche alcuni problemi generali e suggerimenti per l'esecuzione di aggiornamenti di successo che potrebbero essere applicabili in una serie di situazioni diverse, non solo con la distribuzione di Laravel.
Ai fini di questa dimostrazione, utilizzerò un esempio di Laravel, ma tieni presente che qualsiasi applicazione PHP potrebbe utilizzare un approccio simile.
Versione
Per cominciare, è fondamentale conoscere la versione del codice attualmente distribuita in produzione. Può essere incluso in qualche file o almeno nel nome di una cartella o di un file. Per quanto riguarda la denominazione, se seguiamo la pratica standard del versionamento semantico, possiamo includere più informazioni in essa rispetto a un singolo numero.
Osservando due diverse versioni, queste informazioni aggiuntive potrebbero aiutarci a comprendere facilmente la natura delle modifiche introdotte tra di loro.
La versione della versione inizia con un sistema di controllo della versione, come Git. Supponiamo di aver preparato una versione per la distribuzione, ad esempio la versione 1.0.3. Quando si tratta di organizzare queste versioni e flussi di codice, esistono diversi stili di sviluppo come lo sviluppo basato su trunk e il flusso Git, che puoi scegliere o combinare in base alle preferenze del tuo team e alle specifiche del tuo progetto. Alla fine, molto probabilmente finiremo con le nostre versioni contrassegnate in modo corrispondente sul nostro ramo principale.
Dopo il commit, possiamo creare un semplice tag come questo:
git tag v1.0.3
E poi includiamo i tag durante l'esecuzione del comando push:
git push <origin> <branch> --tags
Possiamo anche aggiungere tag ai vecchi commit usando i loro hash.
Ottenere i file di rilascio alla loro destinazione
La distribuzione di Laravel richiede tempo, anche se si tratta semplicemente di copiare i file. Tuttavia, anche se non richiede troppo tempo, il nostro obiettivo è di raggiungere zero tempi di inattività .
Pertanto, dovremmo evitare di installare l'aggiornamento sul posto e non dovremmo modificare i file che vengono serviti in tempo reale. Invece, dovremmo distribuire in un'altra directory ed effettuare il passaggio solo quando l'installazione è completamente pronta.
In realtà, ci sono vari strumenti e servizi che possono aiutarci con le implementazioni, come Envoyer.io (del designer di Laravel.com Jack McDade), Capistrano, Deployer, ecc. Non li ho ancora usati tutti in produzione, quindi non posso dare consigli o scrivere un confronto completo, ma lascia che ti mostri l'idea alla base di questi prodotti. Se alcuni (o tutti) di essi non possono soddisfare le tue esigenze, puoi sempre creare i tuoi script personalizzati per automatizzare il processo nel modo migliore che ritieni opportuno.
Ai fini di questa dimostrazione, supponiamo che la nostra applicazione Laravel sia servita da un server Nginx dal seguente percorso:
/var/www/demo/public
Innanzitutto, abbiamo bisogno di una directory in cui posizionare i file di rilascio ogni volta che eseguiamo una distribuzione. Inoltre, abbiamo bisogno di un collegamento simbolico che punti all'attuale versione funzionante. In questo caso, /var/www/demo
fungerà da nostro collegamento simbolico. La riassegnazione del puntatore ci consentirà di modificare rapidamente le versioni.
Nel caso in cui abbiamo a che fare con un server Apache, potrebbe essere necessario consentire i seguenti collegamenti simbolici nella configurazione:
Options +FollowSymLinks
La nostra struttura può essere qualcosa del genere:
/opt/demo/release/v0.1.0 /opt/demo/release/v0.1.1 /opt/demo/release/v0.1.2
Potrebbero esserci alcuni file che dobbiamo mantenere attraverso diverse distribuzioni, ad esempio file di registro (se non stiamo usando Logstash, ovviamente). Nel caso della distribuzione di Laravel, potremmo voler mantenere la directory di archiviazione e il file di configurazione .env. Possiamo tenerli separati da altri file e utilizzare invece i loro collegamenti simbolici.
Per recuperare i nostri file di rilascio dal repository Git, possiamo utilizzare i comandi clone o archive. Alcune persone usano git clone, ma non puoi clonare un particolare commit o tag. Ciò significa che l'intero repository viene recuperato e quindi viene selezionato il tag specifico. Quando un repository contiene molti rami o una cronologia di grandi dimensioni, la sua dimensione è considerevolmente maggiore dell'archivio di rilascio. Quindi, se non hai specificamente bisogno del repository git in produzione, puoi usare git archive
. Questo ci consente di recuperare solo un archivio di file da un tag specifico. Un altro vantaggio dell'utilizzo di quest'ultimo è che possiamo ignorare alcuni file e cartelle che non dovrebbero essere presenti nell'ambiente di produzione, ad esempio i test. Per questo, dobbiamo solo impostare la proprietà export-ignore nel .gitattributes file
. Nell'elenco di controllo delle pratiche di codifica sicura OWASP è possibile trovare la seguente raccomandazione: "Rimuovere il codice di test o qualsiasi funzionalità non destinata alla produzione, prima della distribuzione".
Se stiamo recuperando la versione dal sistema di controllo della versione sorgente, git archive ed export-ignore potrebbero aiutarci con questo requisito.
Diamo un'occhiata a uno script semplificato (avrebbe bisogno di una migliore gestione degli errori in produzione):
deploy.sh

#!/bin/bash # Terminate execution if any command fails set -e # Get tag from a script argument TAG=$1 GIT_REMOTE_URL='here should be a remote url of the repo' BASE_DIR=/opt/demo # Create folder structure for releases if necessary RELEASE_DIR=$BASE_DIR/releases/$TAG mkdir -p $RELEASE_DIR mkdir -p $BASE_DIR/storage cd $RELEASE_DIR # Fetch the release files from git as a tar archive and unzip git archive \ --remote=$GIT_REMOTE_URL \ --format=tar \ $TAG \ | tar xf - # Install laravel dependencies with composer composer install -o --no-interaction --no-dev # Create symlinks to `storage` and `.env` ln -sf $BASE_DIR/.env ./ rm -rf storage && ln -sf $BASE_DIR/storage ./ # Run database migrations php artisan migrate --no-interaction --force # Run optimization commands for laravel php artisan optimize php artisan cache:clear php artisan route:cache php artisan view:clear php artisan config:cache # Remove existing directory or symlink for the release and create a new one. NGINX_DIR=/var/www/public mkdir -p $NGINX_DIR rm -f $NGINX_DIR/demo ln -sf $RELEASE_DIR $NGINX_DIR/demo
Per distribuire la nostra versione, potremmo semplicemente eseguire quanto segue:
deploy.sh v1.0.3
Nota: in questo esempio, v1.0.3 è il tag git della nostra versione.
Compositore in produzione?
Potresti aver notato che lo script sta invocando Composer per installare le dipendenze. Anche se lo vedi in molti articoli, potrebbero esserci dei problemi con questo approccio. In genere, è consigliabile creare una build completa di un'applicazione e far avanzare questa build attraverso vari ambienti di test dell'infrastruttura. Alla fine, avresti una build completamente testata, che può essere distribuita in sicurezza in produzione. Anche se ogni build dovrebbe essere riproducibile da zero, ciò non significa che dovremmo ricostruire l'app in fasi diverse. Quando facciamo installare il compositore in produzione, questa non è veramente la stessa build di quella testata ed ecco cosa può andare storto:
- L'errore di rete può interrompere il download delle dipendenze.
- Il fornitore della libreria potrebbe non seguire sempre SemVer.
Si può facilmente notare un errore di rete. Il nostro script interromperebbe anche l'esecuzione con un errore. Ma potrebbe essere molto difficile definire una modifica sostanziale in una libreria senza eseguire test, cosa che non è possibile eseguire in produzione. Durante l'installazione delle dipendenze, Composer, npm e altri strumenti simili si basano sul controllo delle versioni semantico: major.minor.patch. Se vedi ~1.0.2 in composer.json, significa installare la versione 1.0.2 o l'ultima versione della patch, come 1.0.4. Se vedi ^1.0.2, significa installare la versione 1.0.2 o l'ultima versione minore o patch, come 1.1.0. Confidiamo che il fornitore della libreria aumenti il numero maggiore quando viene introdotta una modifica sostanziale, ma a volte questo requisito viene mancato o non rispettato. Ci sono stati casi del genere in passato. Anche se inserisci versioni fisse nel tuo composer.json, le tue dipendenze potrebbero avere ~ e ^ nel loro composer.json.
Se è accessibile, secondo me, un modo migliore sarebbe utilizzare un repository di artefatti (Nexus, JFrog, ecc.). La build di rilascio, contenente tutte le dipendenze necessarie, verrebbe creata inizialmente una volta. Questo artefatto verrebbe archiviato in un repository e recuperato da lì per varie fasi di test. Inoltre, questa sarebbe la build da distribuire in produzione, invece di ricostruire l'app da Git.
Mantenere il codice e il database compatibili
Il motivo per cui mi sono innamorato di Laravel a prima vista è stato il modo in cui il suo autore ha prestato molta attenzione ai dettagli, ha pensato alla comodità degli sviluppatori e ha anche incorporato molte best practice nel framework, come le migrazioni di database.
Le migrazioni del database ci consentono di sincronizzare il nostro database e il nostro codice. Entrambe le loro modifiche possono essere incluse in un singolo commit, quindi un'unica versione. Tuttavia, ciò non significa che qualsiasi modifica possa essere implementata senza tempi di inattività. Ad un certo punto durante la distribuzione, ci saranno versioni diverse dell'applicazione e del database in esecuzione. In caso di problemi, questo punto potrebbe anche trasformarsi in un punto. Dovremmo sempre cercare di renderli entrambi compatibili con le versioni precedenti dei loro compagni: vecchio database–nuova app, nuovo database–vecchia app.
Ad esempio, supponiamo di avere una colonna di address
e di doverla suddividere in address1
e address2
. Per mantenere tutto compatibile, potremmo aver bisogno di diverse versioni.
- Aggiungi due nuove colonne nel database.
- Modificare l'applicazione per utilizzare nuovi campi quando possibile.
- Migra i dati degli
address
in nuove colonne e rilasciali.
Questo caso è anche un buon esempio di come piccole modifiche siano molto migliori per la distribuzione. Anche il loro rollback è più facile. Se stiamo modificando la base di codice e il database per diverse settimane o mesi, potrebbe essere impossibile aggiornare il sistema di produzione senza tempi di fermo.
Alcune meraviglie di Kubernetes
Anche se la scalabilità della nostra applicazione potrebbe non richiedere cloud, nodi e Kubernetes, vorrei comunque menzionare l'aspetto delle distribuzioni in K8s. In questo caso, non apportiamo modifiche al sistema, ma dichiariamo cosa vorremmo ottenere e cosa dovrebbe essere in esecuzione su quante repliche. Quindi, Kubernetes si assicura che lo stato effettivo corrisponda a quello desiderato.
Ogni volta che abbiamo una nuova versione pronta, creiamo un'immagine con nuovi file, tagghiamo l'immagine con la nuova versione e la passiamo a K8s. Quest'ultimo girerà rapidamente la nostra immagine all'interno di un cluster. Attenderà prima che l'applicazione sia pronta in base al controllo di prontezza che forniamo, quindi reindirizzerà il traffico in modo invisibile alla nuova applicazione e ucciderà quella vecchia. Possiamo facilmente avere diverse versioni della nostra app in esecuzione che ci permetterebbero di eseguire distribuzioni blu/verde o canary con pochi comandi.
Se sei interessato, ci sono alcune dimostrazioni impressionanti nel discorso "9 Steps to Awesome with Kubernetes di Burr Sutter".