Costruire l'immagine senza toccare il database

Ciao a tutti.

Ho una piccola istanza di Discourse in esecuzione da anni (con praticamente zero problemi): https://discuss.cubeisland.de/.
Ho sempre utilizzato il processo di deployment standard basato sul launcher su una VM dedicata (sul mio hardware in un data center). L’unica modifica apportata nel corso degli anni è stata la migrazione a un database PostgreSQL esterno condiviso.

Recentemente ho iniziato a migrare le applicazioni dalle VM dedicate a un Docker Swarm come passo preparatorio per migrare eventualmente a un cluster Kubernetes, principalmente per risparmiare risorse e rendere alcune parti dell’infrastruttura più “elastiche”.

Oggi ho deciso di occuparmi di questa piccola istanza di Discourse, una delle poche VM dedicate rimaste. “È già in esecuzione su Docker, quanto potrà mai essere difficile deployarla su Swarm?” ho pensato. E da quello che ho letto, in realtà potrebbe esserlo. Potrei semplicemente prendere l’immagine dall’istanza attualmente in esecuzione, caricarla nel nostro registro interno ed eseguirla nel Swarm, e tutto funzionerebbe perfettamente, il che è ottimo.

Ho esaminato i file del launcher, in particolare i template e gli esempi, e ho capito che potrebbe essere una buona idea separare Redis in un tale deployment. Forse potrei anche impostare un job CI per costruire nuove immagini quando aggiungo plugin o quando voglio aggiornare. Quindi ho clonato discourse_docker in locale, copiato la mia definizione esistente del contenitore app.yml nel clone e ho provato a eseguire ./launcher bootstrap app per costruire un’immagine che avrei poi potuto caricare nel mio registro, senza deployarla immediatamente.

A mia sorpresa, lo script ha tentato di connettersi al server PostgreSQL “production” per eseguire la migrazione del database, il che, per fortuna, non è riuscito a fare dal mio workstation locale.

Ho dato un’occhiata qui e sembra che questo sia il modo in cui funziona, il che mi fa chiedersi:

  1. Come si costruisce un contenitore per una nuova istanza, dove non ho ancora un database? Dovrei impostare il database production prima di poter costruire l’immagine?
  2. Immagino che db:migrate venga eseguito solo questa volta, quindi se ho diverse istanze simili (ad esempio prod e test), dovrei aggiornare una delle istanze per costruire la nuova immagine e poi non potrei usare la stessa immagine per la seconda istanza, anche se l’immagine sarebbe identica.
  3. Come si potrebbero costruire immagini per istanze in cui il server database non è accessibile dal sistema che costruisce l’immagine (il che non dovrebbe essere così raro).

Dopo aver letto diversi post (ovviamente incluso questo), sono perfettamente consapevole delle ragioni del processo di build così com’è ora e ne vedo il valore per il 99% delle persone che deployano Discourse in modo casuale sulle loro VM standard complete. Sono anche molto abituato ai modelli di contenitore “all-in-one” e non mi oppongo a questo. Dopo tutto, il valore chiave di Docker deriva dal fatto che il fornitore del software può preconfigurare configurazioni altamente ottimizzate e raggrupparle in un ambiente di esecuzione riproducibile, eliminando la necessità di molte conoscenze specifiche dell’applicazione da parte del team operativo. Quindi sono completamente d’accordo nell’utilizzare gli strumenti forniti da voi; perché dovrei aspettarmi che qualcun altro costruisca contenitori migliori rispetto al fornitore del software stesso? Perché vorrei separare nginx dall’applicazione Ruby, quando non c’è alcun beneficio da guadagnare, solo per rendere il deployment più “puro” (qualunque cosa significhi…)?

Tuttavia, è strano vedere un contenitore che modifica lo stato di runtime mentre è ben lontano dall’esecuzione. Eseguito già diverse applicazioni in contenitori e ne ho containerizzate diverse io stesso, alcune delle quali non erano mai state pensate per essere eseguite in contenitori.

L’esempio principale che mi viene in mente, di un’applicazione che gestisce requisiti/problemi simili a Discourse in modo simile, è GitLab. Anche se ora forniscono un chart Helm elegante per un deployment Kubernetes completamente decomposto “come dovrebbe essere”, indovino (senza guardare numeri specifici) che un simile 99% delle sue distribuzioni di piccole e medie dimensioni utilizza l’immagine Docker omnibus di GitLab (o il pacchetto OS, che è praticamente lo stesso). Hanno un processo di bootstrap simile, ma basato su Chef all’interno del contenitore, che viene eseguito ad ogni avvio e fa le solite cose come le migrazioni del database e la compilazione degli asset.

Sì, l’avvio di GitLab può richiedere diversi minuti a causa di questo, ma non è mai stato un problema per le distribuzioni che ho visto (alcune in aziende più grandi). Soprattutto con moderni sistemi di orchestrazione come Docker Swarm e Kubernetes e altri, che possono eseguire aggiornamenti rolling per te, dove l’istanza vecchia viene spenta solo se la nuova istanza è in esecuzione e supera con successo i controlli di health e readiness, un processo di deployment lungo potrebbe non essere effettivamente un problema. Ma anche senza aggiornamenti rolling sofisticati, che potrebbero o meno funzionare, in molte situazioni si può comunque gestire un certo tempo di inattività.

Quindi: è possibile configurare il launcher per saltare le operazioni dipendenti dal database durante la build dell’immagine e invece eseguire queste operazioni durante l’avvio del contenitore?

Sono sicuramente disposto a investire un po’ di tempo io stesso, ma il mio tempo serale è limitato, quindi qualsiasi indicazione sarebbe molto gradita.

Sono anche aperto a processi completamente diversi se pensate che questa idea sia stupida o non fattibile.

Grazie per qualsiasi feedback!

5 Mi Piace

Volevo fare la stessa cosa che fate voi: gestiamo Discourse su Amazon ECS, quindi avevamo bisogno di poter costruire solo l’immagine web e caricarla su un registry. Non volevo modificare il processo di build di Discourse perché vogliamo rimanere il più possibile vicini all’installazione supportata.

Invece, usiamo lo script launcher normale per costruire un setup a due container su una macchina locale, ma ignoriamo il container dati e cariciamo il container web sul registry. A runtime, sovrascriviamo i dettagli di connessione di Postgres e Redis tramite variabili d’ambiente.

Il deployment della nuova immagine è un processo in tre passaggi:

  1. Esegui le pre-migrazioni sicure. Fai eseguire a ECS questo comando (con la nuova immagine):

     SKIP_POST_DEPLOYMENT_MIGRATIONS=1 rake db:migrate
    
  2. Deploy della nuova immagine. Aggiorna il servizio ECS.

  3. Esegui le post-migrazioni. Fai eseguire a ECS questo comando:

     SKIP_POST_DEPLOYMENT_MIGRATIONS=0 rake db:migrate
    

Avere un container dati locale in esecuzione mentre costruiamo l’immagine è probabilmente uno spreco, ma ci permette di utilizzare il file standard web.template.yml senza dover preoccuparci di quali parti tentano di comunicare con il database o Redis.

8 Mi Piace

Grazie mille! Ho pensato anche io di poter avviare un’istanza di PostgreSQL durante la costruzione dell’immagine e eliminarla non appena completata la build effettiva.

2 Mi Piace

Finalmente ho trovato il tempo per implementarlo!

Ho implementato la build dell’immagine utilizzando una pipeline GitLab CI che avvia PostgreSQL e Redis come servizi durante la build e li rimuove successivamente:

Ora devo solo automatizzare il deployment con le migrazioni del database.

2 Mi Piace

Questa cosa è in esecuzione da oltre un anno senza mai toccarla, nemmeno per la versione 2.8.

2 Mi Piace

Ho spostato la build dell’immagine su GitHub: GitHub - pschichtel/discourse-docker: A reusable Discourse container built using the launcher tool.

L’immagine è pubblicata su pschichtel/discourse:stable-web_only

sembra che questo abbia finalmente causato un problema. durante l’aggiornamento da 3.0.6 a 3.1.0, non sono state eseguite migrazioni del database. Eseguire il comando finale bundle exec rake db:migrate all’interno del container in esecuzione ha funzionato, sebbene solo dopo un altro riavvio del container.

Devi migrare di nuovo quando la nuova immagine è stata avviata senza che quell’env fosse impostato. Esiste un rake task che lo farà, ma non riesco a ricordarlo né a trovarlo dal mio telefono. Qualcosa come ensure_post_migrations.

Per quel che vale, non ho notato alcun problema. Seguo principalmente la branch di rilascio beta e, per quanto ne so, le migrazioni sono state eseguite correttamente per ogni passaggio della serie 3.1.0.beta…

Ho trovato db:ensure_post_migrations tramite rake -AT.

Qual è la differenza tra db:migrate con SKIP_POST_DEPLOYMENT_MIGRATIONS=0 e db:ensure_post_migrations?

Ok, dopo aver esaminato il codice, ho capito cosa fa db:ensure_post_migrations. Dovrebbe essere utilizzato nella stessa esecuzione di rake prima di db:migrate per garantire che SKIP_POST_DEPLOYMENT_MIGRATIONS sia impostato su 0. Il mio script lo garantisce già:

il .gitlab-ci.yml:

./migrate.sh pre || echo "Redis non in esecuzione durante le pre-migrazioni, saltando..."
docker stack deploy --prune --resolve-image always -c "$STACK.yml" "$STACK"
./docker-stack-wait.sh -t 180 "$STACK"
./migrate.sh post

il migrate.sh:

#!/usr/bin/env sh

if [ "$(docker ps -q --filter "label=com.docker.stack.namespace=${STACK}" --filter "label=com.docker.swarm.service.name=${STACK}_${DISCOURSE_REDIS_HOST}" | wc -l)" = "0" ]
then
    echo "Nessun container redis trovato, impossibile eseguire le migrazioni!"
    exit 1
fi

if [ "$1" = "pre" ]
then
    skip_post=1
else
    skip_post=0
fi


docker run \
    --rm \
    --name "discourse-migration-${DISCOURSE_DB_HOST}-${DISCOURSE_DB_NAME}" \
    --network "${STACK}_discourse" \
    --workdir /var/www/discourse \
    -u discourse \
    -e SKIP_POST_DEPLOYMENT_MIGRATIONS="$skip_post" \
    -e LANG="${LANG}" \
    -e DISCOURSE_DEFAULT_LOCALE="${DISCOURSE_DEFAULT_LOCALE}" \
    -e DISCOURSE_HOSTNAME="${DISCOURSE_HOSTNAME}" \
    -e DISCOURSE_DEVELOPER_EMAILS="${DISCOURSE_DEVELOPER_EMAILS}" \
    -e DISCOURSE_SMTP_ADDRESS="${DISCOURSE_SMTP_ADDRESS}" \
    -e DISCOURSE_SMTP_PORT="${DISCOURSE_SMTP_PORT}" \
    -e DISCOURSE_DB_USERNAME="${DISCOURSE_DB_USERNAME}" \
    -e DISCOURSE_DB_PASSWORD="${DISCOURSE_DB_PASSWORD}" \
    -e DISCOURSE_DB_HOST="${DISCOURSE_DB_HOST}" \
    -e DISCOURSE_DB_NAME="${DISCOURSE_DB_NAME}" \
    -e DISCOURSE_REDIS_HOST="${DISCOURSE_REDIS_HOST}" \
    "$DISCOURSE_IMAGE" \
    bundle exec rake db:migrate

Esegue db:migrate con SKIP_POST_DEPLOYMENT_MIGRATIONS=1 nella nuova immagine sul docker swarm mentre discourse sta ancora eseguendo la vecchia versione. Quindi distribuisce la nuova immagine allo swarm e attende che converga. Alla fine, esegue nuovamente db:migrate, ma con SKIP_POST_DEPLOYMENT_MIGRATIONS=0.

Questo ha funzionato in modo affidabile per ogni versione per oltre 2 anni. Dato che ha funzionato per te @simonk, hai fatto qualcosa di fondamentalmente diverso rispetto al mio script?

1 Mi Piace

No, sto ancora seguendo lo stesso processo che ho delineato qui sopra, che per quanto ne so è più o meno lo stesso del tuo. Uso un semplice rake db:migrate invece di bundle exec rake db:migrate, ma non riesco a immaginare che ciò possa fare molta differenza.

Non ho mai usato docker stack o swarm. C’è qualche possibilità di un bug da qualche parte nei tuoi script che potrebbe causare allo script migrate.sh l’utilizzo della vecchia immagine invece di quella nuova?

Non l’ho controllato esplicitamente, ci darò un’occhiata. Lo swarm utilizzerà sicuramente l’immagine più recente, ma forse lo script CI per qualche motivo non ha utilizzato quella più recente.

Ho controllato ora con l’aggiornamento 3.1.1. In effetti, lo script CI utilizzava una versione precedente del container.