Générer l'image sans toucher la base de données

Bonjour à tous.

J’ai une instance Discourse assez petite qui tourne depuis des années (avec pratiquement zéro problème) : https://discuss.cubeisland.de/.
J’utilise le processus de déploiement standard basé sur le launcher sur une VM dédiée (sur mon propre matériel dans un data center). La seule modification que j’ai apportée au fil des ans a été de migrer vers une base de données PostgreSQL externe partagée.

Récemment, j’ai commencé à migrer des applications depuis des VM dédiées vers un swarm Docker, comme étape préparatoire avant de migrer vers un cluster Kubernetes, principalement pour économiser des ressources et rendre certaines parties de l’infrastructure plus « élastiques ».

Aujourd’hui était le jour où je me suis penché sur cette petite instance Discourse, l’une des rares VM d’application dédiées restantes. « Elle tourne déjà sur Docker, comment cela pourrait-il être difficile de la déployer dans un swarm ? », me suis-je dit. Et d’après ce que j’ai lu, ce serait en effet le cas. Je peux simplement prendre l’image de l’instance actuellement en cours d’exécution, la pousser vers notre registre interne et l’exécuter dans le swarm, et tout fonctionnera parfaitement, ce qui est formidable.

J’ai examiné les fichiers du launcher, en particulier les modèles et les exemples, et j’ai pensé qu’il serait probablement judicieux de séparer Redis dans un tel déploiement, et peut-être que je pourrais configurer un job CI pour construire de nouvelles images lorsque j’ajoute des plugins ou que je souhaite mettre à jour. J’ai donc cloné discourse_docker localement, copié ma définition de conteneur app.yml existante dans le clone et essayé d’exécuter ./launcher bootstrap app pour construire une image que je pourrais ensuite pousser vers mon registre, sans la déployer immédiatement.

À ma grande surprise, le script a tenté de se connecter au serveur PostgreSQL « production » pour migrer la base de données, ce qui, heureusement, n’était pas possible depuis mon poste de travail local.

J’ai regardé autour de moi ici et apparemment c’est ainsi que cela fonctionne, ce qui me fait me demander :

  1. Comment construire un conteneur pour une nouvelle instance, où je n’ai pas encore de base de données ? Devrais-je configurer la base de données de production avant de pouvoir construire l’image ?
  2. Je suppose que c’est la seule fois où db:migrate est exécuté, donc si j’ai plusieurs instances similaires (par exemple, prod et test), je devrais mettre à niveau l’une des instances pour construire la nouvelle image, puis je ne pourrais pas utiliser la même image pour la seconde instance, même si l’image serait identique.
  3. Comment procéder pour construire des images pour des instances où le serveur de base de données n’est pas accessible depuis le système qui construit l’image (ce qui ne devrait pas être si rare).

Après avoir lu plusieurs publications (évidemment y compris celle-ci), je suis parfaitement conscient des raisons du processus de construction tel qu’il est actuellement et je vois la valeur de celui-ci pour les 99 % de personnes qui déploient Discourse de manière occasionnelle sur leur VM standard complète. Et je suis très habitué aux modèles de conteneurs « tout-en-un » et je ne m’y oppose pas. Après tout, la valeur clé de Docker réside dans le fait que le fournisseur de logiciels peut pré-configurer des configurations hautement optimisées et les regrouper dans un environnement d’exécution reproductible, éliminant ainsi le besoin de nombreuses connaissances très spécifiques à l’application du côté des opérations. Je suis donc tout à fait partant pour utiliser vos outils fournis ; pourquoi m’attendrais-je à ce que quelqu’un d’autre construise de meilleurs conteneurs que le fournisseur de logiciels lui-même ? Pourquoi voudrais-je séparer nginx et l’application Ruby, alors qu’il n’y a aucun avantage à en tirer, juste pour rendre le déploiement plus « pur » (quoi que cela signifie…) ?

Cependant, il est étrange de voir un conteneur qui modifie l’état d’exécution alors qu’il n’est même pas en cours d’exécution. J’exécute déjà pas mal d’applications dans des conteneurs et j’en ai moi-même conteneurisé plusieurs, dont certaines n’étaient pas destinées à fonctionner dans des conteneurs.

L’exemple principal qui me vient à l’esprit, d’une application qui traite des exigences/problèmes similaires de manière similaire à Discourse, est Gitlab. Bien qu’ils fournissent désormais un charmant graphique Helm pour un déploiement Kubernetes pleinement décomposé « tel que cela devrait être », je suppose (sans regarder les chiffres) qu’une proportion similaire de 99 % de ses déploiements de taille petite à moyenne utilisent l’image Docker omnibus de Gitlab (ou le paquet OS, qui est pratiquement la même chose). Ils ont un processus de démarrage similaire, mais basé sur Chef à l’intérieur du conteneur, qui est tout exécuté à chaque démarrage et effectue les tâches habituelles comme les migrations de base de données et la compilation des assets.

Oui, le démarrage de Gitlab peut prendre plusieurs minutes à cause de cela, mais cela n’a jamais été un problème pour les déploiements que j’ai vus (certains dans de grandes entreprises). Surtout avec les systèmes d’orchestration modernes comme Docker Swarm et Kubernetes, etc., qui peuvent exécuter des mises à jour en roulement pour vous, où l’ancienne instance n’est éteinte que si la nouvelle instance est en cours d’exécution et a passé avec succès les vérifications de santé et de disponibilité, un processus de déploiement long pourrait en réalité ne pas être un problème. Mais même sans mises à jour en roulement sophistiquées, qui peuvent ou non fonctionner, vous pouvez aussi vous en sortir avec pas mal de temps d’arrêt dans de nombreuses situations.

Donc : est-il possible de configurer le launcher pour sauter les opérations dépendantes de la base de données pendant la construction de l’image et effectuer ces opérations au démarrage du conteneur à la place ?

Je suis certainement prêt à investir un peu de temps moi-même, mais mon temps le soir est limité, donc toute indication serait la bienvenue.

Je suis également ouvert à des processus complètement différents si vous pensez que c’est stupide ou même pas possible, etc.

Merci pour tout retour d’information !

5 « J'aime »

Je voulais faire la même chose que vous : nous exécutons Discourse sur Amazon ECS, nous devions donc pouvoir construire uniquement l’image web et la pousser vers un registre. Je n’avais pas envie de modifier le processus de build de Discourse, car nous souhaitons rester aussi proches que possible de l’installation prise en charge.

À la place, nous utilisons le script launcher standard pour construire une configuration à deux conteneurs sur une machine locale, mais nous ignorons le conteneur de données et nous poussons le conteneur web vers le registre. Au moment de l’exécution, nous remplaçons les détails de connexion à Postgres et Redis via des variables d’environnement.

Le déploiement de la nouvelle image se fait en trois étapes :

  1. Exécuter les pré-migrations sécurisées. Demandez à ECS d’exécuter cette commande (avec la nouvelle image) :

     SKIP_POST_DEPLOYMENT_MIGRATIONS=1 rake db:migrate
    
  2. Déployer la nouvelle image. Mettez à jour le service ECS.

  3. Exécuter les post-migrations. Demandez à ECS d’exécuter cette commande :

     SKIP_POST_DEPLOYMENT_MIGRATIONS=0 rake db:migrate
    

Avoir un conteneur de données local en cours d’exécution pendant la construction de l’image est probablement inutile, mais cela nous permet d’utiliser le fichier web.template.yml standard sans nous soucier des parties qui tentent de se connecter à la base de données ou à Redis.

8 « J'aime »

Merci pour cela ! J’ai aussi pensé que je pourrais simplement lancer un Postgres pendant la construction de l’image et le supprimer une fois la construction de la build réelle terminée.

2 « J'aime »

J’ai enfin pris le temps de mettre cela en œuvre !

J’ai implémenté la construction de l’image à l’aide d’un pipeline GitLab CI qui exécute PostgreSQL et Redis en tant que services pendant la construction, puis les supprime ensuite :

Il ne me reste plus qu’à automatiser le déploiement avec les migrations de base de données.

2 « J'aime »

Cette chose fonctionne depuis plus d’un an sans qu’on y touche jamais, pas même pour la version 2.8.

2 « J'aime »

J’ai déplacé la construction de l’image vers github : GitHub - pschichtel/discourse-docker: A reusable Discourse container built using the launcher tool.

L’image est publiée sur pschichtel/discourse:stable-web_only

Il semble que cela ait finalement cassé. Lors de la mise à niveau de 3.0.6 vers 3.1.0, aucune migration de base de données n’a été effectuée. L’exécution du dernier bundle exec rake db:migrate à l’intérieur du conteneur en cours d’exécution a cependant fonctionné, bien qu’après un autre redémarrage du conteneur.

Vous devrez migrer à nouveau lorsque la nouvelle image aura démarré sans cette variable d’environnement définie. Il existe une tâche rake qui fera cela, mais je ne m’en souviens pas et je ne peux pas la trouver depuis mon téléphone. Quelque chose comme ensure_post_migrations.

Pour ce que ça vaut, je n’ai remarqué aucune défaillance. Je suis principalement la branche de publication bêta, et pour autant que je sache, les migrations se sont déroulées correctement à chaque étape de la série 3.1.0.beta…

J’ai trouvé db:ensure_post_migrations via rake -AT.

Quelle est la différence entre db:migrate avec SKIP_POST_DEPLOYMENT_MIGRATIONS=0 et db:ensure_post_migrations ?

Ok, après avoir examiné le code, je comprends ce que fait db:ensure_post_migrations. Il est censé être utilisé dans la même exécution de rake avant db:migrate pour s’assurer que SKIP_POST_DEPLOYMENT_MIGRATIONS est défini sur 0. Mon script s’en assure déjà :

le .gitlab-ci.yml :

./migrate.sh pre || echo "Redis not running during pre migrations, skipping..."
docker stack deploy --prune --resolve-image always -c "$STACK.yml" "$STACK"
./docker-stack-wait.sh -t 180 "$STACK"
./migrate.sh post

le 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 "No redis container found, unable to run migrations!"
    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

Il exécute db:migrate avec SKIP_POST_DEPLOYMENT_MIGRATIONS=1 dans la nouvelle image sur le docker swarm pendant que discourse exécute toujours l’ancienne version. Ensuite, il déploie la nouvelle image sur le swarm et attend qu’elle converge. À la fin, il exécute à nouveau db:migrate, mais avec SKIP_POST_DEPLOYMENT_MIGRATIONS=0.

Cela a fonctionné de manière fiable pour chaque version depuis plus de 2 ans maintenant. Étant donné que cela a fonctionné pour vous @simonk, avez-vous fait quelque chose de fondamentalement différent par rapport à mon script ?

1 « J'aime »

Non, je suis toujours le même processus que j’ai décrit ici, qui, à ma connaissance, est à peu près le même que le tien. J’utilise un rake db:migrate simple plutôt que bundle exec rake db:migrate, mais je ne peux pas imaginer que cela fasse une grande différence.

Je n’ai jamais utilisé docker stack ou swarm. Y a-t-il une possibilité de bug quelque part dans tes scripts qui pourrait faire en sorte que le script migrate.sh utilise l’ancienne image plutôt que la nouvelle ?

Je n’ai pas vérifié cela explicitement, je vais examiner la question. Le swarm utilisera certainement la dernière image, mais le script CI n’a peut-être pas utilisé la dernière pour une raison quelconque.

J’ai examiné cela maintenant avec la mise à jour 3.1.1. En effet, le script CI utilisait une ancienne version du conteneur.