Installation auf Kubernetes

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.

Vielen Dank an alle für die hilfreichen Antworten. Ich habe noch ein paar Fragen dazu, wie man das richtig macht.

Ich versuche, eine minimale app.yaml für das Bootstrapping vorzubereiten, die nur die für den Container-Build erforderlichen Informationen enthält. Einige der darin enthaltenen Informationen sind eindeutig für die Container-Laufzeit gedacht – wie die Volume-Mounts und Port-Mappings. Bei den Umgebungsvariablen bin ich mir jedoch nicht sicher. Ich werde es einfach versuchen, aber werden diese Umgebungsvariablen während des Container-Builds verwendet (irgendwie in das Dockerfile injiziert) oder erst zur Laufzeit des Containers? Falls Letzteres zutrifft, werde ich sicherstellen, dass sie in die entsprechende k8s-Konfigurationsdatei gelangen.

Zweitens: Einige Leute hier haben davon gesprochen, das Image in ein privates Container-Repository zu pushen. Ist das erforderlich? Anders ausgedrückt – enthält das Build-Image vertrauliche Informationen, die nicht in einem öffentlichen Repository wie Docker Hub veröffentlicht werden sollten? (Wir haben noch kein privates Container-Repository und möchten dessen Einrichtung gerne vermeiden.)

Schließlich: Gibt es eine app.yaml-Einstellung, um den Namen des erstellten Containers zu steuern? Das wäre eher eine Sache der Feinabstimmung, aber es wäre schön :slight_smile:.

Vielen Dank im Voraus für die Hilfe! (Entschuldigung, dass ich einen alten Thread wiederbelebe. Dies ist das erste Ergebnis bei Google, wenn man nach der Installation von Discourse auf Kubernetes sucht.)

@Geoffrey_Challen Du kannst ein Image mit dem Discourse-Repository und den Plugins erstellen, die Ruby-Gems und anderen Abhängigkeiten installieren und es in eine Registry (wie DockerHub) hochladen. Dieses Repository wäre umgebungsunabhängig und könnte öffentlich sein (sofern du kein privates Plugin oder Ähnliches einbeziehst). Dieses Basis-Image könnte in Staging- und Produktionsumgebungen sowie in verschiedenen Projekten verwendet werden (sofern sie dieselben Plugins nutzen).

Schritte wie das Vorkompilieren der Assets, die Datenbankmigration und das Erstellen des SSL-Zertifikats sollten jedoch auf dem Zielrechner ausgeführt werden, um das endgültige Image zu generieren.

Ich weiß nicht genau, wie man das in einem k8s-Cluster integriert. Ich habe mich für den konservativen Ansatz entschieden und mich an den offiziellen Leitfaden des Discourse-Teams gehalten, wobei ich die Schritte in zwei Teile aufgeteilt habe.

Bei diesem Teil bin ich mir nicht sicher, ob ich ihn richtig verstehe. Passen diese Vorgänge nicht automatisch innerhalb des Containers, wie sie benötigt werden? Ich hoffe, ich kann das einfach in unsere Cloud hochladen und muss nie wieder Shell-Zugriff auf die Maschine erhalten – ähnlich wie ich selten (wenn überhaupt) das Discourse Docker-Container betreten muss.

Sie müssen den Container nicht explizit eingeben. Was ich damit meine, ist, dass Sie damit kein vorkompiliertes Image erstellen können (z. B. in einer CI-Pipeline generiert) und es direkt verwenden können, da dies auf dem Zielrechner ausgeführt werden muss, auf dem sich die Datenbank befindet (dies könnte automatisiert werden, aber ich habe dies in k8s noch nicht umgesetzt, obwohl ich es mit Ansible getan habe).

Ah, okay. Ich verwende die All-in-One-Vorlagen, um die Datenbank in den Container einzubinden. Das ist für unseren Anwendungsfall geeignet, bei dem es um Klassen mit 10 bis 1000 Schülern geht – zumindest hat die All-in-One-Lösung in meiner Klasse in dieser Konfiguration gut funktioniert. Die Datenbank befindet sich also im Container.

Aber unabhängig davon: Führt Discourse beim Start des Containers nicht die Datenbank-Migrationen oder andere Einrichtungsschritte aus?

Bist du sicher, dass sich die Datenbank im Container befindet? Oder ist es das RDBMS (in diesem Fall PostgreSQL)? Die unterstützte Installation verwendet die Datenbank außerhalb des Containers (was erwartet wird) und mappet ein Volume von innen nach außen (auf den Host). Zudem würde nach einem Neuaufbau des Containers dieser neu erstellt werden und du würdest alle Daten verlieren.

Wenn die Datenbank wirklich im Container liegt, weiß ich nicht genau, wie du basierend auf der offiziellen Installation ein Upgrade durchführen könntest, da das Launcher-Skript beim Neuaufbau den Container mehrmals erstellt und zerstört (und mit --rm ausgeführt wird, was bedeutet, dass du nach dem Stoppen des Containers alle Daten, einschließlich der Datenbank, verlieren würdest).

Ich habe nicht versucht, die Art und Weise zu ändern, wie der Neuaufbau durchgeführt wird, aber angenommen, du könntest ihn so anpassen, dass alles im Container läuft, ohne ihn neu zu erstellen, dann solltest du in der Lage sein, den Container in eine Registry zu pushen (stelle sicher, dass sie privat ist, da die Geheimnisse dort enthalten wären). Dennoch empfehle ich diesen Ansatz aus mehreren Gründen nicht (einige davon wurden bereits erwähnt).

Die Standardinstallation enthält nginx, Rails, Postgres und Redis innerhalb des Containers. Für die Daten von Postgres und Redis werden externe Volumes verwendet. Diese werden bei einem Neuaufbau oder Upgrade nicht zerstört.

Ja, es ist nur seltsam, dass er sagte, die Datenbank liege innerhalb des Containers, es sei denn, er hat die Funktionsweise der Standardinstallation geändert oder meinte PostgreSQL, nicht die Datenbank selbst.

Nein – die Migrationen und Schritte zur Kompilierung von Assets erfolgen während der Phase ./launcher bootstrap, nachdem Plugins aufgelöst wurden. Sobald dies abgeschlossen ist, kann der Container beliebig oft neu gestartet werden, oder die Web-Prozesse können auf mehrere Maschinen verteilt werden, usw.


Im Idealfall sollte die Einrichtung etwa so aussehen:

  • Offizielle Builds stellen das Discourse-Basis-Image bereits bereit.
    • Wir benötigen zudem einen Container für discourse_docker, der über eine eigene Docker-Installation verfügt.
  • Einrichtung eines privaten Registries innerhalb des Clusters.
  • Einrichtung eines ConfigMap mit dem Inhalt der app.yml.
  • Ausführung eines Jobs, der ./launcher bootstrap unter Verwendung von verschachteltem Docker auf einem VM-basierten Knoten (ohne Zugriff auf den Docker-Socket) ausführt, das resultierende Image umbenennt und in das private Registry pusht (mit einem zeitstempelbasierten Label, nicht latest; local_discourse ist hier kein guter Name) und das Deployment auf das neue Label rollt.
    • Wow, das sind wirklich viele Berechtigungen für den Upgrade-Job.

Postgres läuft im Container. Es speichert die Daten außerhalb des Containers, aber es läuft im Container, wenn du den Standard-Installationsvorlagen folgst. Gleiches gilt für Redis. Ich denke, die Verwirrung entsteht, wenn ich sage „die Datenbank läuft im Container

Ich denke, das ergibt für mich jetzt mehr Sinn, besonders nach dem Lesen der langen verlinkten Diskussion zu docker-compose, dem Launcher-Skript usw.

Hier ist, was ich gerne tun möchte:

  • ./launcher bootstrap lokal ausführen, um ein „fat

Vielleicht interessieren Sie sich für eine Multi-Site-Konfiguration.

Sie stoßen auf zwei große Probleme: Discourse ist noch nicht für Kubernetes bereit, sodass benutzerdefinierter Code erforderlich ist. Außerdem bewegen Sie sich in den Bereich, in dem das Discourse-Team Geld verdient (Betreuung einer großen Anzahl von Foren), sodass das Unterstützungsniveau sinken wird.

Mein Rat? Führen Sie eine Multi-Site-Konfiguration mit statischer Planung auf VMs durch, die sich vollständig außerhalb Ihres Clusters befinden. (Oder nutzen Sie einen Service vom Typ ExternalName, der auf die VM zeigt, um denselben Ingress beizubehalten.)

OK… Ich habe einen Weg gefunden, dies zu bewerkstelligen. Ich bin nicht zu 100 % damit zufrieden, aber es funktioniert und könnte für andere ansprechend sein, die versuchen, ein einfaches Single-Container-Fat-Image-Deployment (inklusive PostgreSQL, Redis usw.) von Discourse in Kubernetes durchzuführen.

Mein Ansatz

Nach der Untersuchung des Bootstrapping-Prozesses wurde mir klar, dass dieser leider zwei verschiedene Arten von Operationen vermischt: solche, die nur den zugrunde liegenden Container betreffen, und andere, die in die umgebende Umgebung eingreifen, hauptsächlich über die /shared-Volume-Mount, in der sich die PostgreSQL-Datendateien befinden. Anstatt zu versuchen, diese Schritte auseinanderzuziehen, scheint es vernünftiger, die Bootstrapping-Schritte einfach in der Umgebung auszuführen, in der der Container tatsächlich bereitgestellt wird.

Leider möchte launcher bootstrap einen Container erstellen und Docker verwenden. Das bedeutet, dass das Ausführen von launcher innerhalb eines anderen Containers (zum Beispiel in einem Container, der in unserer Cloud läuft) entweder eine Docker-in-Docker-Konfiguration erfordert (machbar, aber nicht als Best Practice angesehen) oder den zugrunde liegenden Docker-Daemon offenlegt. Ich bin mir nicht einmal sicher, ob der zweite Ansatz funktionieren würde, da ich denke, dass er eine Volume-Mount-Operation gegen das lokale Dateisystem des Knotens interpretieren würde, während wir in unserem Szenario /shared auf ein persistentes Kubernetes-Volume mappen möchten. Vielleicht würde die Docker-in-Docker-Methode funktionieren, aber dann hätte man auch eine seltsame dreifache Volume-Mapping vom inneren verschachtelten Container in den äußeren Container und von dort zum persistenten Kubernetes-Volume. Das klingt… unklug.

Im Wesentlichen erstellt launcher bootstrap jedoch eine große .yml-Datei, indem er den templates-Wert in der app.yml verarbeitet und diese dann an das Discourse-Basisimage übergibt, sobald der Bootstrap-Prozess abgeschlossen ist. Wenn wir also die Konfigurationsdatei extrahieren können, können wir die Konfiguration auf jedem Rechner generieren und müssen nur noch herausfinden, wie wir sie an einen Container übergeben, den wir in der Cloud starten.

Hier ist also eine Übersicht über die Schritte, die wir befolgen werden:

  1. Generieren der Bootstrap-Konfiguration mit einer modifizierten launcher
  2. Übergabe dieser an ein modifiziertes Discourse-Basisimage, das den Bootstrap-Prozess (mittels pups) durchführt und dann Discourse startet

Generieren der Bootstrap-Konfiguration

Hier ist die erforderliche Änderung an launcher, um einen dump-Befehl zu unterstützen, der die zusammengeführte Konfiguration auf STDOUT schreibt:

run_dump() {
  set_template_info
  echo "$input"
}

(Beachten Sie, dass dieser Befehl in unserem Fork von discourse_docker verfügbar ist.)

Der erste Schritt besteht also darin, den neuen launcher dump-Befehl zu verwenden, um unsere Bootstrap-Konfiguration zu erstellen:

# Ersetzen Sie app durch den Namen Ihrer Container-Konfiguration
./launcher dump app > bootstrap.yml

Erstellen des initialen Containers

Als Nächstes benötigen wir einen Container, der weiß, dass er pups ausführen muss, um den Container zu bootstrappen, bevor er über /sbin/boot gestartet wird. Ich habe folgende Dockerfile verwendet, um eine kleine Änderung am Basis-Discourse-Image vorzunehmen:

FROM discourse/base:2.0.20191219-2109
COPY scripts/bootstrap.sh /
CMD bash bootstrap.sh

Dabei enthält scripts/bootstrap.sh:

cd /pups/ && /pups/bin/pups --stdin < /bootstrap/bootstrap.yml && /sbin/boot

Ich habe dies als geoffreychallen:discourse_base:2.0.20191219-2109 veröffentlicht. (Beachten Sie, dass Sie wahrscheinlich dasselbe auch erreichen könnten, indem Sie den Boot-Befehl des Basis-Discourse-Docker-Images ändern, aber ich hatte Schwierigkeiten, dies mit der Shell-Umleitung zum Lesen der Konfigurationsdatei durch pups zum Laufen zu bringen.)

Kubernetes-Konfiguration

Jetzt benötigen wir unsere Kubernetes-Konfiguration. Meine sieht wie folgt aus:

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

Ihre wird anders aussehen. Beachten Sie, dass ich HTTPS upstream terminiere, daher die Änderungen an der Ingress-Konfiguration. Ich lege auch gerne alles in eine Datei, lösche Teile, die nicht funktionieren, während ich iteriere, und lasse Kubernetes dann Duplikate bei der nächsten kubectl create -f überspringen. Beachten Sie auch, dass ich replicas: 0 gesetzt habe, damit die Bereitstellung nicht sofort startet, sobald sie konfiguriert ist. Das liegt daran, dass wir noch einen zusätzlichen Konfigurationsschritt abschließen müssen.

Ich habe die Liste der Umgebungsvariablen aus dem übernommen, was ich gesehen habe, wie sie von launcher start an den Container übergeben wurden. Ich weiß nicht, ob alle notwendig sind und je nach Ihrer Konfiguration andere fehlen könnten. Wie Sie es handhaben (YMMV).

Beachten Sie, dass wir zwei Volume-Mappings haben, die in den Container zeigen: Das erste ist für PostgreSQL, konfiguriert als persistentes Volume, das bei Pod-Neustarts erhalten bleibt. Das zweite ist eine Konfigurationszuordnung, die wie folgt erstellt wurde:

kubectl create configmap kotlin-forum-bootstrap --from-file=bootstrap.yml=<path/to/bootstrap.yml>

Dabei muss kotlin-forum-bootstrap mit Ihrer Kubernetes-Konfiguration übereinstimmen und path/to/bootstrap.yml ist der Pfad zur bootstrap.yml-Datei, die wir oben mit launcher dump erstellt haben.

Sobald Ihre configmap vorhanden ist, sollten Sie in der Lage sein, Ihre Bereitstellung auf eine Replik zu skalieren und zu sehen, wie Discourse den gleichen Bootstrap-Prozess startet und ausführt, den launcher bootstrap durchgeführt hätte. Das dauert ein paar Minuten. Wenn dies abgeschlossen ist, wird Ihre Discourse-Installation gestartet.

Weitere Konfigurationsdetails

Ein paar weitere Hinweise, die mir auf dem Weg zur vollständigen Konfiguration (zumindest vorerst) begegnet sind:

  • Alle Upstream-Proxy-Server müssen X-Forwarded-Header weiterleiten, einschließlich sowohl X-Forwarded-For, X-Forwarded-Proto als auch X-Forwarded-Port. Andernfalls kommt es zu seltsamen Authentifizierungsfehlern beim Versuch, die Google-Anmeldung und wahrscheinlich andere Anmeldeanbieter zu verwenden.
  • Ihr nginx-Ingress-Controller muss so konfiguriert sein, dass er Header weiterleitet, indem er use-forwarded-headers in der globalen ConfigMap setzt. Das hat eine Weile gedauert, bis es richtig lief, da ich mindestens mehrmals die falsche ConfigMap bearbeitet habe und dann erwartet habe, dass meine Ingress-Container neu starten, wenn sich die ConfigMap ändert. (Das taten sie nicht.)

Aktualisieren

Um die bereitgestellte Installation zu aktualisieren, generieren Sie die neue bootstrap.yml-Datei neu, aktualisieren Sie die ConfigMap und starten Sie dann den Container neu (am einfachsten durch Skalieren auf 0 und dann zurück auf 1 Replik).

Dies führt zu einer kurzen Ausfallzeit, da das Bootstrapping stattfindet, bevor der Container erstellt wird. Das scheint mir in Fällen unvermeidlich zu sein, in denen Sie die Konfiguration aktualisieren und/oder das Basisimage ändern müssen. launcher rebuild ist als stop; bootstrap; start dokumentiert, was bedeutet, dass der Bootstrap-Prozess auch dann zu Ausfallzeiten führt, wenn er über das Launcher-Skript ausgeführt wird.

Kommentare

Dieses Fat-Container-Discourse-Bereitstellungsmuster wäre viel einfacher zu unterstützen, wenn das launcher-Skript sauberer trennen würde zwischen (a) Bootstrap-Schritten, die offline durchgeführt werden können und nur die Dateien im Container betreffen, und (b) Bootstrap-Schritten, die die Datenbank oder andere Zustände außerhalb des Containers modifizieren oder darauf zugreifen müssen. Der oben beschriebene Ansatz ist etwas frustrierend, weil man allerlei JS-Uglification, Asset-Minifizierung und andere Dinge sieht, die mit dem vorherigen Deployment durchgeführt werden könnten… aber sie sind einfach zu stark mit anderen Dingen (wie Datenbank-Migrationen) vermischt, die ohne Zugriff auf die Datenbank nicht durchgeführt werden können. Ich habe kurz darüber nachgedacht, einen Container zu erstellen, der nur die Schritte in templates/postgres.yml ausführt, habe dann aber festgestellt, dass Datenbank-Migrationen vom Web-Template durchgeführt wurden, habe über Plugins nachgedacht und dann einfach aufgegeben :slight_smile:.

Mit einer besseren Trennung könnte die Neubereitstellung für Fat-Container so funktionieren:

  • Neues Fat-Container offline erstellen und alle internen Schritte im Container durchführen
  • Diesen Container veröffentlichen
  • Wenn bereit für das Upgrade, den vorherigen Container stoppen, den neuen Fat-Container starten und ihm erlauben, alle Schritte abzuschließen, die Datenbankzugriff benötigen. Basierend auf meinen Experimenten scheinen diese schneller zu sein als einige der anderen Bootstrap-Schritte.

Das würde zu etwas weniger Ausfallzeit führen. Es lohnt sich wahrscheinlich allein aus diesem Grund nicht, aber ich kann mir vorstellen, dass dies auch komplexere Bereitstellungsszenarien mit gemeinsamen Datenbanken oder ähnlichem vereinfachen könnte.

Das ergibt mehr Sinn. Das habe ich getan, als ich den Bootstrap-Prozess in zwei Schritte aufgeteilt habe. Der erste kann in einer isolierten Umgebung (wie einer CI-Pipeline) ausgeführt werden, um ein Basis-Image mit dem Discourse-Repository, den Gems und installierten Plugins zu erstellen. Der zweite Schritt muss auf der Zielmaschine ausgeführt werden (oder zumindest Zugriff auf die Produktionsdatenbank haben), um die Datenbankmigration durchzuführen und die Assets zu generieren (dies erfolgt im Bootstrap-Prozess, nicht beim Starten des Containers).

Ja, das wäre großartig. Ich habe das bereits angefragt, aber ich weiß nicht, ob und wann das umgesetzt wird.

Das vollständig in einer separaten Umgebung umzusetzen, wäre schwierig, da die Aufgabe zum Precompilieren der Assets Zugriff auf die Datenbank benötigt (für Dinge wie benutzerdefiniertes CSS). Es wäre jedoch großartig, wenn nur der Teil, der von der Datenbank abhängt, in einem separaten Schritt erledigt werden könnte (und alle anderen Assets, die nicht von der Datenbank abhängen, separat vorkompiliert werden könnten). Ich weiß jedoch nicht, wie technisch umsetzbar das wäre.

Das ist im Wesentlichen das, was ich bei den von mir durchgeführten Kubernetes-Installationen mache. Ich kann mir nicht vorstellen, wie oder warum man Kubernetes ohne separate Daten- und Web-Container (oder eine andere Art von externem PostgreSQL und Redis) einsetzen sollte – die Installationen, die ich für Kunden durchgeführt habe, nutzen dafür GCP-Ressourcen.

Außerdem gibt es eine Umgebungsvariable namens skip_post_migration_updates, die man für echte Upgrades ohne Ausfallzeit verstehen muss. Sie wird hier beschrieben.