Yes
Whatever ENV you specify when you finally do you docker run on the image … will take.
Yes
Whatever ENV you specify when you finally do you docker run on the image … will take.
And doing those things at every boot of the container is reasonable?
Some people like this pattern, I do not, that is why rails introduces some super fancy locking around migrations to ensure they never run concurrently when a cluster of 20 app try to run migrations at the same time.
Some people just boot the same image and run the migrations in some out-of-band process.
But each container should still rake assets:precompile at least once?
Depends on how do you host those.
If you have a NFS share of some sort (like AWS EFS) or if you upload those to some object storage service that can only be done on bootstrap.
Merci à tous pour toutes ces réponses utiles. J’ai encore quelques questions sur la manière de procéder correctement.
J’essaie de préparer un fichier app.yaml minimal pour l’amorçage, ne contenant que les informations requises pour la construction du conteneur. Certaines des informations qu’il contient sont clairement destinées à l’exécution du conteneur, comme les montages de volumes et les mappages de ports. Mais je ne suis pas sûr pour les variables d’environnement. Je suppose que je vais simplement essayer, mais ces variables d’environnement sont-elles utilisées lors de la construction du conteneur (injectées dans le Dockerfile d’une manière ou d’une autre) ou uniquement au moment de l’exécution du conteneur ? Si c’est le cas, je m’assurerai qu’elles se retrouvent dans le fichier de configuration k8s approprié.
Deuxièmement, certaines personnes ici ont parlé de pousser l’image vers un référentiel de conteneurs privé. Est-ce obligatoire ? Autrement dit, l’image de construction contient-elle des informations secrètes qui ne devraient pas être publiées sur un référentiel public comme Docker Hub ? (Nous n’avons pas encore de référentiel de conteneurs privé et j’aimerais éviter d’en configurer un.)
Enfin, existe-t-il un paramètre app.yaml pour contrôler le nom du conteneur créé ? C’est plutôt une question de finition, mais ce serait agréable
.
Merci d’avance pour votre aide ! (Désolé de faire remonter un ancien fil. C’est le premier résultat sur Google lors de la recherche sur la façon d’installer Discourse sur Kubernetes.)
@Geoffrey_Challen Vous pouvez créer une image avec le dépôt Discourse et les plugins, installer les gems Ruby et autres dépendances, puis la pousser vers un registre (comme DockerHub). Ce dépôt serait agnostique à l’environnement et pourrait être public (sauf si vous incluez un plugin privé ou autre chose de ce genre). Cette image de base pourrait être utilisée dans des environnements de staging et de production, voire dans différents projets (s’ils utilisent les mêmes plugins).
Des étapes comme la précompilation des assets, la migration de la base de données et la génération du certificat SSL doivent toutefois être exécutées sur la machine cible pour générer l’image finale.
Je ne sais pas exactement comment intégrer cela dans un cluster k8s. J’ai opté pour une approche conservatrice en suivant le guide officiel de l’équipe Discourse, en séparant simplement le processus en deux étapes.
Cette partie, je ne suis pas sûr de bien la comprendre. Ces opérations ne se produisent-elles pas automatiquement à l’intérieur du conteneur au besoin ? J’espère pouvoir simplement pousser cela dans notre cloud sans jamais avoir besoin d’accéder au shell de la machine, un peu comme je n’ai rarement (voire jamais) besoin d’entrer dans le conteneur Docker de Discourse.
Vous n’avez pas besoin de spécifier explicitement le conteneur. Ce que je veux dire, c’est que vous ne pouvez pas générer une image précompilée avec eux (générée dans un pipeline CI, par exemple) et l’utiliser telle quelle, car cela doit être exécuté sur la machine cible, où se trouve la base de données (cela pourrait être automatisé, mais je ne l’ai pas fait dans k8s, bien que je l’aie fait avec Ansible).
Ah, d’accord. J’utilise les modèles tout-en-un pour inclure la base de données dans le conteneur. Cela convient à notre cas d’usage, qui implique des classes de 10 à 1000 étudiants. Du moins, le modèle tout-en-un a bien fonctionné pour ma classe dans cette configuration. Donc la base de données est à l’intérieur du conteneur.
Mais peu importe, Discourse ne lance-t-il pas les migrations de base de données ou d’autres étapes de configuration au démarrage du conteneur ?
Êtes-vous certain que la base de données se trouve à l’intérieur du conteneur ? Ou s’agit-il du SGBD (dans ce cas, PostgreSQL) ? L’installation prise en charge utilise une base de données externe au conteneur (ce qui est attendu), en mappant un volume interne vers l’extérieur (l’hôte). De plus, après une reconstruction du conteneur, celui-ci est recréé et vous perdriez toutes les données.
Si la base de données est vraiment à l’intérieur du conteneur, je ne sais pas exactement comment vous pourriez procéder à une mise à niveau basée sur l’installation officielle, car le script launcher semble créer et détruire le conteneur plusieurs fois lors de la reconstruction (et s’exécute avec --rm, ce qui signifie que vous perdriez toutes vos données, y compris la base de données, après l’arrêt du conteneur).
Je n’ai pas essayé de modifier la façon dont la reconstruction est effectuée, mais en supposant que vous puissiez la modifier pour exécuter tout à l’intérieur du conteneur sans le recréer, vous devriez pouvoir pousser le conteneur vers un registre (assurez-vous qu’il est privé, car les secrets s’y trouveraient). Cela dit, je ne recommande pas cette approche pour plusieurs raisons (certaines mentionnées précédemment).
L’installation standard inclut nginx, Rails, PostgreSQL et Redis à l’intérieur du conteneur. Elle utilise des volumes externes pour les données de PostgreSQL et Redis. Ces volumes ne sont pas supprimés lors d’une reconstruction ou d’une mise à niveau.
Oui, c’est juste étrange qu’il ait dit que la base de données se trouve à l’intérieur du conteneur, sauf s’il a changé la façon dont l’installation standard fonctionne, ou s’il voulait dire PostgreSQL, et non la base de données elle-même.
Non - les étapes de migrations et de compilation des assets se déroulent pendant la phase ./launcher bootstrap, après la résolution des plugins. Une fois cela terminé, le conteneur peut être redémarré autant de fois que nécessaire, ou les processus web peuvent être répartis sur plusieurs machines, etc.
En imaginant la configuration, cela devrait ressembler à ceci :
./launcher bootstrap en utilisant le Docker imbriqué sur un nœud basé sur une VM (sans accès au socket Docker), puis renomme et pousse l’image résultante vers le registre privé (avec un libellé basé sur un horodatage, et non latest) (local_discourse n’est pas un bon nom ici) et met à jour le déploiement vers le nouveau libellé
Postgres s’exécute à l’intérieur du conteneur. Il enregistre les données en dehors du conteneur, mais il s’exécute à l’intérieur si vous utilisez le jeu standard de modèles d’installation. Idem pour Redis. Je pense que la confusion vient du fait que quand je dis « la base de données s’exécute dans le conteneur », je parle du serveur de base de données, même si les fichiers de la base de données sont stockés en dehors du conteneur. (Mais les fichiers de la base de données ne « s’exécutent » pas, c’est pourquoi je considère ma formulation claire — mais apparemment pas assez claire
.)
PS : en fait, il n’enregistre pas nécessairement les données en dehors du conteneur, sauf si vous configurez Docker pour monter en liaison ce répertoire. J’ai pu sauter cette étape lors du bootstrap, bien que ce ne soit probablement pas une bonne idée, car dans ce cas, le contenu de la base de données ne survivra pas aux redémarrages du conteneur.
Je pense que cela commence à avoir plus de sens pour moi maintenant, surtout après avoir lu la longue conversation liée concernant docker-compose, le script de lancement, etc.
Voici ce que je souhaiterais pouvoir faire :
./launcher bootstrap localement pour créer une image Discourse « complète » incluant toutes les dépendances : postgres, redis, etc../launcher bootstrap pour mettre à jour l’image et redéployer sans détruire les données (évidemment)Ma compréhension est que l’image Discourse complète ne devrait nécessiter aucune dépendance de service externe. Cependant, pour que les données survivent aux mises à jour des conteneurs, les fichiers de la base de données postgres doivent être stockés en dehors du conteneur. Ce n’est pas un problème — je peux créer un volume persistant k8s pour eux.
Voici maintenant le seul problème que j’anticipe. La plupart des opérations lors de ./launcher bootstrap ne touchent que des fichiers situés à l’intérieur du conteneur. Par exemple, la précompilation des assets. Ce n’est pas grave, car les résultats restent dans le conteneur et n’ont pas besoin de survivre aux mises à jour.
La grande exception ici est la migration de la base de données. Cette étape doit avoir accès à la base de données qui sera utilisée après la fin du bootstrap. Donc, pour moi, cela semble être le principal obstacle au déploiement facile d’images Discourse complètes dans le cloud.
J’ai remarqué que @sam a mentionné à plusieurs reprises qu’il redéploie Discourse pour ses clients en utilisant un flux de travail à peu près similaire à celui que j’ai décrit ci-dessus. Mais je soupçonne que la raison pour laquelle cela fonctionne est que leurs images Discourse sont configurées pour utiliser un serveur de base de données (et probablement Redis également) qui s’exécute sur leur cluster — ce qui serait logique pour prendre en charge plusieurs déploiements, mais ce n’est pas tout à fait ce que je veux faire. Cela signifie que le processus de bootstrap peut modifier la base de données de production — ou peut-être que l’étape de migration de la base de données est tout simplement ignorée, car les mises à jour et les migrations de la base de données sont gérées de manière externe. @sam : pourriez-vous confirmer ?
Quoi qu’il en soit, la conclusion pour moi est que je dois trouver un moyen d’exécuter les migrations de la base de données au démarrage du conteneur, et non pendant ./launcher bootstrap. Je suppose qu’à ce stade, une façon de faire serait :
./launcher bootstrap, avec un montage de volume pointant vers une base de données locale vide, puisque cette base de données ne sera pas utilisée plus tard. Cela permettrait de mettre tout en place dans le conteneur, sans pour autant terminer le travail sur postgres.Vous pourriez être intéressé par une configuration multi-sites.
Vous rencontrez deux gros problèmes : Discourse n’est pas prêt pour Kubernetes, ce qui nécessite du code personnalisé. De plus, vous entrez dans le domaine où l’équipe de Discourse génère des revenus (hébergement d’un grand nombre de forums), ce qui entraînera une baisse du niveau de support dont vous bénéficiez.
Mon conseil ? Mettez en place une configuration multi-sites avec un ordonnancement statique sur des machines virtuelles, entièrement en dehors de votre cluster. (Ou utilisez un Service de type ExternalName pointant vers la machine virtuelle pour conserver le même Ingress.)
OK… J’ai réussi à trouver une façon de faire cela. Je n’en suis pas totalement satisfait, mais cela fonctionne et cela pourrait plaire à d’autres personnes qui tentent un déploiement simple d’une image Discourse « tout-en-un » (incluant PostgreSQL, Redis, etc.) dans un conteneur unique sur Kubernetes.
Après avoir examiné le processus de démarrage, il m’est devenu clair que, malheureusement, il mélange deux types d’opérations différentes : celles qui n’affectent que le conteneur sous-jacent, et d’autres qui interagissent avec l’environnement extérieur, principalement via le montage du volume /shared où résident les fichiers de données de PostgreSQL. Plutôt que d’essayer de démêler ces étapes, il semble plus logique d’exécuter les étapes de démarrage directement dans l’environnement où le conteneur sera effectivement déployé.
Malheureusement, launcher bootstrap souhaite créer un conteneur et utiliser Docker. Ainsi, exécuter launcher à l’intérieur d’un autre conteneur (par exemple, dans un conteneur tournant sur notre cloud) implique soit de s’embrouiller dans une configuration Docker-in-Docker (possible, mais pas considérée comme une bonne pratique), soit d’exposer le démon Docker sous-jacent. Je ne suis même pas certain que cette seconde approche fonctionnerait, car je pense qu’elle interpréterait un montage de volume par rapport au système de fichiers local du nœud, alors que dans notre scénario, nous voulons monter /shared sur un volume Kubernetes persistant. Peut-être que la voie Docker-in-Docker fonctionnerait, mais alors vous auriez aussi un étrange triple montage de volume : depuis le conteneur imbriqué vers le conteneur extérieur, puis de là vers le volume Kubernetes persistant. Cela semble… peu judicieux.
Cependant, essentiellement, launcher bootstrap génère un grand fichier .yml en traitant la valeur templates dans app.yml, puis le transmet à l’image de base de Discourse une fois le processus de démarrage terminé. Donc, si nous pouvons extraire le fichier de configuration, nous pouvons générer la configuration sur n’importe quelle machine, et il ne nous reste plus qu’à trouver comment la transmettre à un conteneur que nous lançons dans le cloud.
Ainsi, pour résumer, voici les étapes que nous allons suivre :
launcherpups) puis lancera DiscourseVoici le changement nécessaire dans launcher pour prendre en charge une commande dump qui écrit la configuration fusionnée sur la sortie standard (STDOUT) :
run_dump() {
set_template_info
echo "$input"
}
(Veuillez noter que cette commande est disponible dans notre fork de discourse_docker.)
La première étape consiste donc à utiliser la nouvelle commande launcher dump ajoutée ci-dessus pour créer notre configuration de démarrage :
# Remplacez « app » par le nom de votre configuration de conteneur
./launcher dump app > bootstrap.yml
Ensuite, nous avons besoin d’un conteneur qui sait exécuter pups pour démarrer le conteneur avant de lancer via /sbin/boot. J’ai utilisé le Dockerfile suivant pour apporter une petite modification à l’image de base de Discourse :
FROM discourse/base:2.0.20191219-2109
COPY scripts/bootstrap.sh /
CMD bash bootstrap.sh
Où scripts/bootstrap.sh contient :
cd /pups/ && /pups/bin/pups --stdin < /bootstrap/bootstrap.yml && /sbin/boot
J’ai publié cela sous le nom geoffreychallen:discourse_base:2.0.20191219-2109. (Notez que vous pourriez probablement accomplir la même chose en modifiant la commande de démarrage de l’image Docker de base de Discourse, mais j’ai eu du mal à faire en sorte que cela fonctionne avec la redirection de shell requise pour que pups lise le fichier de configuration.)
Maintenant, nous avons besoin de notre configuration Kubernetes. La mienne ressemble à ceci :
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: kotlin-forum-pvc
namespace: ikp
spec:
storageClassName: rook-ceph-block
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 64Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kotlin-forum-deployment
namespace: ikp
spec:
replicas: 0
selector:
matchLabels:
app: kotlin-forum
template:
metadata:
labels:
app: kotlin-forum
spec:
volumes:
- name: kotlin-forum
persistentVolumeClaim:
claimName: kotlin-forum-pvc
- name: bootstrap
configMap:
name: kotlin-forum-bootstrap
containers:
- name: kotlin-forum
image: geoffreychallen/discourse_base:2.0.20191219-2109
imagePullPolicy: Always
volumeMounts:
- name: kotlin-forum
mountPath: /shared/
- name: bootstrap
mountPath: /bootstrap/
ports:
- containerPort: 80
env:
- name: TZ
value: "America/Chicago"
- name: LANG
value: en_US.UTF-8
- name: RAILS_ENV
value: production
- name: UNICORN_WORKERS
value: "3"
- name: UNICORN_SIDEKIQS
value: "1"
- name: RUBY_GLOBAL_METHOD_CACHE_SIZE
value: "131072"
- name: RUBY_GC_HEAP_GROWTH_MAX_SLOTS
value: "40000"
- name: RUBY_GC_HEAP_INIT_SLOTS
value: "400000"
- name: RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR
value: "1.5"
- name: DISCOURSE_DB_SOCKET
value: /var/run/postgresql
- name: DISCOURSE_DEFAULT_LOCALE
value: en
- name: DISCOURSE_HOSTNAME
value: kotlin-forum.cs.illinois.edu
- name: DISCOURSE_DEVELOPER_EMAILS
value: challen@illinois.edu
- name: DISCOURSE_SMTP_ADDRESS
value: outbound-relays.techservices.illinois.edu
- name: DISCOURSE_SMTP_PORT
value: "25"
---
apiVersion: v1
kind: Service
metadata:
name: kotlin-forum
namespace: ikp
spec:
type: NodePort
ports:
- name: http
port: 80
targetPort: 80
selector:
app: kotlin-forum
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
namespace: ikp
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
name: kotlin-forum-ingress
spec:
rules:
- host: kotlin-forum.cs.illinois.edu
http:
paths:
- backend:
serviceName: kotlin-forum
servicePort: 80
La vôtre sera différente. Notez que je termine le HTTPS en amont, d’où les modifications apportées à la configuration Ingress. J’aime aussi tout mettre dans un seul fichier, supprimer les parties qui ne fonctionnent pas au fur et à mesure que j’itére, puis laisser Kubernetes ignorer les doublons lors du prochain kubectl create -f. Notez également que j’ai défini replicas: 0 afin que le déploiement ne démarre pas dès qu’il est configuré. C’est parce que nous avons une dernière étape de configuration à terminer.
J’ai copié la liste des variables d’environnement à partir de ce que j’ai vu être transmis au conteneur par launcher start. Je ne sais pas si toutes sont nécessaires et d’autres pourraient manquer selon votre configuration. À vous de voir.
Notez que nous avons deux montages de volume pointant vers le conteneur : le premier est pour PostgreSQL, configuré comme un volume persistant qui survivra aux redémarrages des pods. Le second est un mappage de configuration créé comme ceci :
kubectl create configmap kotlin-forum-bootstrap --from-file=bootstrap.yml=<path/to/bootstrap.yml>
Où kotlin-forum-bootstrap doit correspondre à votre configuration Kubernetes et path/to/bootstrap.yml est le chemin vers le fichier bootstrap.yml que nous avons créé en utilisant launcher dump ci-dessus.
Une fois votre configmap en place, vous devriez pouvoir mettre à l’échelle votre déploiement à un seul réplica et voir Discourse démarrer et exécuter le même processus de démarrage que launcher bootstrap aurait effectué. Cela prend quelques minutes. Une fois cela terminé, votre installation Discourse sera opérationnelle.
Quelques autres remarques que j’ai rencontrées en chemin pour obtenir cette configuration (du moins pour l’instant) complètement terminée :
X-Forwarded, y compris X-Forwarded-For, X-Forwarded-Proto et X-Forwarded-Port. Ne pas le faire entraînera des erreurs d’authentification étranges lors de l’utilisation de la connexion Google et probablement d’autres fournisseurs de connexion.nginx doit être configuré pour transmettre les en-têtes en définissant use-forwarded-headers dans le ConfigMap global. Cela m’a pris un certain temps à régler correctement, car à plusieurs reprises, j’ai édité le mauvais ConfigMap, puis m’attendais à ce que mes conteneurs d’entrée redémarrent lorsque le ConfigMap changeait. (Ce n’était pas le cas.)Pour mettre à jour l’installation déployée, vous régénérez le nouveau fichier bootstrap.yml, mettez à jour le ConfigMap, puis redémarrez le conteneur (le plus facilement en passant à 0 réplica puis de nouveau à 1).
Cela entraîne un peu d’interruption de service, car le démarrage a lieu avant que le conteneur ne soit construit. Mais cela me semble inévitable dans les cas où vous devez mettre à jour la configuration et/ou changer l’image de base. launcher rebuild est documenté comme stop; bootstrap; start, ce qui signifie que le processus de démarrage entraînera toujours une interruption de service, même s’il est effectué via le script launcher.
Ce modèle de déploiement Discourse en conteneur « tout-en-un » serait beaucoup plus facile à prendre en charge si le script launcher séparait plus clairement (a) les étapes de démarrage qui pourraient être effectuées hors ligne et n’affecteraient que les fichiers dans le conteneur et (b) les étapes de démarrage qui modifient ou nécessitent l’accès à la base de données ou à d’autres états hors du conteneur. L’approche décrite ci-dessus est un peu frustrante car vous voyez toutes sortes d’obfuscation JS, de minification d’assets et d’autres choses qui pourraient être faites avec le déploiement précédent en cours d’exécution… mais elles sont trop mélangées avec d’autres éléments (comme les migrations de base de données) qui ne peuvent pas être effectués sans accès à la base de données. J’ai brièvement envisagé de créer un conteneur qui n’effectuerait que les étapes dans templates/postgres.yml, mais j’ai ensuite remarqué que les migrations de base de données étaient effectuées par le modèle web, j’ai pensé aux plugins, puis j’ai simplement abandonné
.
Avec une meilleure séparation, le redéploiement pour les conteneurs « tout-en-un » pourrait fonctionner comme ceci :
Cela entraînerait un peu moins d’interruption de service. Ce n’est probablement pas la peine d’y consacrer tant d’efforts pour cette raison seule, mais je peux imaginer que cela pourrait aussi simplifier des scénarios de déploiement plus complexes impliquant des bases de données partagées ou autre chose.
Cela a plus de sens. C’est ce que j’ai fait en séparant l’étape de démarrage en deux phases. La première peut s’exécuter dans un environnement isolé (comme un pipeline CI) pour générer une image de base avec le dépôt Discourse, les gems et les plugins installés. La deuxième étape doit s’exécuter sur la machine cible (ou au moins avoir accès à la base de données de production) pour effectuer la migration de la base de données et générer les ressources (cela est fait lors du processus de démarrage, et non au lancement du conteneur).
Oui, ce serait formidable. Je l’ai déjà demandé, mais je ne sais pas si et quand cela sera réalisé.
Ce serait difficile à implémenter complètement dans un environnement séparé car la tâche de précompilation des ressources nécessite un accès à la base de données (pour des éléments comme le CSS personnalisé), mais ce serait idéal si seules les parties dépendant de la base de données pouvaient être traitées dans une étape séparée (et que toutes les autres ressources, qui ne dépendent pas de la base de données, puissent être précompilées séparément, bien que je ne sache pas dans quelle mesure cela serait techniquement réalisable).
C’est à peu près ce que je fais sur les installations Kubernetes que j’ai réalisées. Je ne vois pas comment ni pourquoi utiliser k8s sans conteneurs de données et de web séparés (ou une autre forme de PostgreSQL et Redis externes — les installations que j’ai effectuées pour des clients utilisent des ressources GCP pour cela).
De plus, il existe une variable d’environnement skip_post_migration_updates qu’il faut comprendre pour des mises à jour réellement sans temps d’arrêt. Elle est décrite ici.