Installazione su 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.

Grazie a tutti per tutte le risposte utili. Ho ancora alcune domande su come farlo correttamente.

Sto cercando di preparare un app.yaml minimale per l’avvio che contenga solo le informazioni necessarie per la build del container. Alcune delle informazioni in esso contenute sono chiaramente destinate all’esecuzione del container, come i mount dei volumi e le mappature delle porte. Ma non sono sicuro riguardo alle variabili d’ambiente. Immagino che proverò semplicemente, ma queste variabili d’ambiente vengono utilizzate durante la build del container (iniettate nel Dockerfile in qualche modo) o solo al momento dell’esecuzione del container? Se è quest’ultimo caso, mi assicurerò che finiscano nel file di configurazione k8s appropriato.

In secondo luogo, alcune persone qui hanno parlato di spingere l’immagine in un repository privato di container. È obbligatorio? In altre parole, l’immagine di build contiene informazioni segrete che non dovrebbero essere pubblicate in un repository pubblico come Docker Hub? (Non abbiamo ancora un repository privato di container e vorrei evitare di allestirne uno.)

Infine, esiste un’impostazione app.yaml per controllare il nome del container creato? È più una questione di rifinitura, ma sarebbe bello :slight_smile:.

Grazie in anticipo per l’aiuto! (Scusa per aver rimesso in cima un vecchio thread. Questo è il primo risultato su Google quando si cerca come installare Discourse su Kubernetes.)

@Geoffrey_Challen Puoi creare un’immagine con il repository di Discourse e i plugin, installare le gemme Ruby e le altre dipendenze e caricarla su un registro (come DockerHub). Questo repository sarebbe agnostico rispetto all’ambiente e potrebbe essere pubblico (a meno che tu non includa un plugin privato o qualcosa di simile). Questa immagine di base potrebbe essere utilizzata negli ambienti di staging e produzione e persino in progetti diversi (se utilizzano gli stessi plugin).

Passaggi come la precompilazione delle risorse, la migrazione del database e la generazione del certificato SSL dovrebbero essere eseguiti invece sulla macchina di destinazione, per generare l’immagine finale.

Non so esattamente come includerlo in un cluster k8s, però. Ho optato per un approccio conservativo e l’ho utilizzato seguendo la guida ufficiale del team di Discourse, semplicemente separandolo in due passaggi.

Questa parte non sono sicuro di averla capita. Questi passaggi non avverranno automaticamente all’interno del container, se necessario? Spero di poter semplicemente caricare tutto nel nostro cloud senza mai dover accedere via shell alla macchina, proprio come raramente (se non mai) devo entrare nel container Docker di Discourse.

Non è necessario specificare esplicitamente il contenitore. Ciò che intendo è che non è possibile generare un’immagine precompilata con essi (ad esempio, generata in una pipeline CI) e utilizzarla così com’è, poiché questa deve essere eseguita sulla macchina di destinazione, dove si trova il database (questo potrebbe essere automatizzato, ma non l’ho fatto in k8s, anche se l’ho fatto con Ansible).

Ah, OK. Sto utilizzando i template all-in-one per includere il database nel container. Questa soluzione è adatta al nostro caso d’uso, che prevede classi con un numero di studenti compreso tra 10 e 1000. Almeno finora, il modello all-in-one ha funzionato bene per la mia classe in questa configurazione. Quindi il database si trova all’interno del container.

Comunque, Discourse non esegue le migrazioni del database o altre operazioni di configurazione all’avvio del container?

Sei sicuro che il database sia all’interno del container? O intendi il sistema di gestione del database (in questo caso, PostgreSQL)? L’installazione supportata utilizza il database esterno al container (come previsto), mappando un volume interno verso l’esterno (l’host). Inoltre, dopo una ricostruzione del container, quest’ultimo viene ricreato e perderesti tutti i dati.

Se il database è davvero all’interno del container, non so esattamente come potresti eseguire un aggiornamento basato sull’installazione ufficiale, poiché lo script launcher sembra creare e distruggere il container più volte durante la ricostruzione (e viene eseguito con --rm, il che significa che perderesti tutti i tuoi dati, incluso il database, dopo l’arresto del container).

Non ho provato a modificare il modo in cui viene eseguita la ricostruzione, ma supponendo che tu sia in grado di adattarlo in modo che tutto venga eseguito all’interno del container senza ricrearlo, dovresti essere in grado di inviare il container a un registro (assicurati che sia privato, poiché le chiavi segrete sarebbero lì). Detto questo, non consiglio questo approccio per diversi motivi (alcuni dei quali menzionati in precedenza).

L’installazione standard include nginx, Rails, PostgreSQL e Redis all’interno del container. Utilizza volumi esterni per i dati di PostgreSQL e Redis. Non viene distrutto durante una ricostruzione o un aggiornamento.

Sì, è strano che abbia detto che il database si trova all’interno del container, a meno che non abbia modificato il funzionamento dell’installazione standard o intendesse PostgreSQL e non il database stesso.

No - le migrazioni e le fasi di compilazione degli asset avvengono durante la fase ./launcher bootstrap, dopo che i plugin sono stati risolti. Una volta completato questo passaggio, il container può essere riavviato quante volte necessario, oppure i processi web possono essere distribuiti su più macchine, ecc.


In teoria, la configurazione dovrebbe essere più o meno così:

  • le build ufficiali forniscono già l’immagine base di Discourse.
    • sarà inoltre necessario un container per discourse_docker che includa Docker al suo interno
    • creare un registro privato all’interno del cluster
    • creare un ConfigMap con il contenuto di app.yml
    • eseguire un Job che esegue ./launcher bootstrap utilizzando Docker nidificato su un nodo basato su VM (senza accesso al socket Docker), rinomina e spinge l’immagine risultante nel registro privato (con un’etichetta basata sul timestamp, non latest) (local_discourse non è un nome adatto in questo caso) e aggiorna il deployment alla nuova etichetta
      • wow, sono molte le autorizzazioni richieste per il job di aggiornamento.

Postgres viene eseguito all’interno del container. Salva i dati all’esterno del container, ma viene eseguito all’interno se si utilizza l’insieme standard di modelli di installazione. Lo stesso vale per Redis. Credo che la confusione nasca dal fatto che quando dico “il database viene eseguito all’interno del container” mi riferisco al server di database, anche se i file del database risiedono all’esterno del container. (Ma i file del database non “vengono eseguiti”, motivo per cui considero la mia formulazione chiara — ma evidentemente non abbastanza :slight_smile:.)

PS: in realtà, non salva necessariamente i dati all’esterno del container a meno che tu non configuri Docker per montare in bind quella directory. Sono riuscito a saltare questo passaggio durante il bootstrap, anche se probabilmente non è una buona idea, poiché in quel caso i contenuti del database non sopravviveranno ai riavvii del container.

Penso che ora abbia più senso per me, specialmente dopo aver letto la lunga conversazione collegata riguardo a docker-compose, lo script di avvio, ecc.

Ecco cosa vorrei poter fare:

  • Eseguire ./launcher bootstrap localmente per creare un’immagine Discourse “completa” che includa tutte le dipendenze: postgres, redis, ecc.
  • Distribuire quell’immagine su Kubernetes
  • In un secondo momento rieseguire ./launcher bootstrap per aggiornare l’immagine e ridistribuirla senza distruggere i dati (ovvio)

La mia comprensione è che l’immagine Discourse completa non dovrebbe richiedere dipendenze da servizi esterni. Tuttavia, affinché i dati sopravvivano agli aggiornamenti dei container, i file del database postgres devono risiedere al di fuori del container. Questo non è un problema: posso creare un volume persistente k8s per loro.

Ora ecco l’unico problema che prevedo. La maggior parte di ciò che accade durante ./launcher bootstrap tocca solo file che risiedono all’interno del container. Ad esempio, la precompilazione delle risorse. Va bene, poiché i risultati risiedono all’interno del container e non devono sopravvivere agli aggiornamenti.

L’eccezione principale qui è la migrazione del database. Questo passaggio deve avere accesso al database che verrà utilizzato dopo il completamento del bootstrap. Quindi, per me, questo sembra essere il principale ostacolo alla distribuzione facile di immagini Discourse complete nel cloud.

Ho notato che @sam ha menzionato più volte di ridistribuire Discourse per i propri clienti utilizzando un flusso di lavoro approssimativamente simile a quello che ho descritto sopra. Ma sospetto che il motivo per cui questo funzioni è che le loro immagini Discourse sono configurate per utilizzare un server di database (e probabilmente anche Redis) che viene eseguito sul loro cluster, il che avrebbe senso per supportare più distribuzioni ma non è esattamente ciò che voglio fare. Ciò significa che il processo di bootstrap può modificare il database di produzione, oppure che il passaggio di migrazione del database viene saltato completamente poiché gli aggiornamenti e le migrazioni del database sono gestiti esternamente. @sam: potresti confermare?

Comunque, la conseguenza pratica per me è che devo trovare un modo per eseguire le migrazioni del database quando il container viene avviato, non durante ./launcher bootstrap. Immagino che a quel punto un modo per farlo sarebbe:

  • Costruire il container Discourse completo localmente usando ./launcher bootstrap, utilizzando un mount di volume che punta a un database locale vuoto, poiché quel database non verrà utilizzato in seguito. Questo metterebbe tutto a posto nel container, senza completare solo il lavoro su postgres.
  • Trovare un modo per eseguire il passaggio di migrazione del database sul database di produzione effettivo, forse usando un container di inizializzazione k8s?
  • Sostituire la vecchia immagine Discourse con quella nuova

Potresti essere interessato a una configurazione multisito.

Ci sono due grandi problemi che stai incontrando: Discourse non è pronto per Kubernetes, quindi è necessario del codice personalizzato. Inoltre, ti stai avvicinando a ciò che il team di Discourse fa per guadagnare (ospitare un gran numero di forum), quindi il livello di supporto che riceverai diminuirà.

Il mio consiglio? Esegui una configurazione multisito con pianificazione statica su VM, completamente al di fuori del tuo cluster. (O un Service di tipo ExternalName che punta alla VM per mantenere lo stesso Ingress.)

OK… Sono riuscito a trovare un modo per farlo. Non ne sono completamente soddisfatto, ma funziona e potrebbe essere interessante per altri che stanno cercando di implementare un’immagine Discourse “monolitica” (che include PostgreSQL, Redis, ecc.) in un singolo contenitore su Kubernetes.

Il mio approccio

Dopo aver esaminato il processo di avvio, mi è diventato chiaro che, purtroppo, questo mescola due tipi diversi di operazioni: alcune che influenzano solo il contenitore sottostante e altre che interagiscono con l’ambiente circostante, principalmente attraverso il montaggio del volume /shared dove risiedono i file di dati di PostgreSQL. Invece di cercare di separare questi passaggi, sembra più ragionevole eseguire semplicemente i passaggi di avvio nell’ambiente in cui il contenitore verrà effettivamente distribuito.

Purtroppo, launcher bootstrap vuole creare un contenitore e utilizzare Docker. Quindi, eseguire launcher all’interno di un altro contenitore (ad esempio, in un contenitore in esecuzione sul nostro cloud) significa o impelagarsi in una configurazione Docker-in-Docker (fattibile, ma non considerata una buona pratica) o esporre il demone Docker sottostante. Non sono nemmeno sicuro che il secondo approccio funzionerebbe, poiché penso che interpretarebbe un montaggio del volume rispetto al filesystem locale del nodo, mentre nel nostro scenario vogliamo montare /shared su un volume Kubernetes persistente. Forse la strada Docker-in-Docker potrebbe funzionare, ma allora avresti anche un bizzarro triplo montaggio del volume: dal contenitore nidificato a quello esterno e da lì al volume Kubernetes persistente. Sembra… poco saggio.

Tuttavia, essenzialmente launcher bootstrap crea un unico grande file .yml elaborando il valore templates in app.yml e lo passa all’immagine base di Discourse al termine del processo di avvio. Quindi, se riusciamo a estrarre il file di configurazione, possiamo generarlo su qualsiasi macchina e poi dobbiamo solo capire come passarlo a un contenitore che avvieremo nel cloud.

Quindi, come panoramica, ecco i passaggi che seguiremo:

  1. Generare la configurazione di avvio utilizzando una versione modificata di launcher
  2. Passarla a un’immagine base di Discourse modificata che eseguirà l’avvio (usando pups) e poi avvierà Discourse

Generazione della configurazione di avvio

Ecco la modifica necessaria a launcher per supportare un comando dump che scrive la configurazione unita sullo STDOUT:

run_dump() {
  set_template_info
  echo "$input"
}

(Tieni presente che questo comando è disponibile nel nostro fork di discourse_docker.)

Quindi, il primo passaggio è utilizzare il nuovo comando launcher dump aggiunto sopra per creare la nostra configurazione di avvio:

# Sostituisci con il nome della tua configurazione del contenitore per app
./launcher dump app > bootstrap.yml

Creazione iniziale del contenitore

Successivamente, abbiamo bisogno di un contenitore che sappia eseguire pups per avviare il contenitore prima di avviarsi tramite /sbin/boot. Ho utilizzato il seguente Dockerfile per apportare una piccola modifica all’immagine base di Discourse:

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

Dove scripts/bootstrap.sh contiene:

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

Ho pubblicato questo come geoffreychallen:discourse_base:2.0.20191219-2109. (Tieni presente che probabilmente potresti ottenere lo stesso risultato modificando il comando di avvio dell’immagine Docker base di Discourse, ma avevo difficoltà a farlo funzionare con la reindirizzazione della shell necessaria per far leggere a pups il file di configurazione.)

Configurazione Kubernetes

Ora abbiamo bisogno della nostra configurazione Kubernetes. La mia è così:

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 tua sarà diversa. Tieni presente che sto terminando l’HTTPS a monte, da qui le modifiche alla configurazione Ingress. Mi piace anche mettere tutto in un unico file, cancellare i pezzi che non funzionano mentre itero, e poi lasciare che Kubernetes salti i duplicati al prossimo kubectl create -f. Nota anche che ho impostato replicas: 0 in modo che la distribuzione non inizi non appena viene configurata. Questo perché abbiamo un ulteriore elemento di configurazione da completare.

Ho copiato l’elenco delle variabili d’ambiente da ciò che ho visto passare al contenitore tramite launcher start. Non so se tutti questi siano necessari e altri potrebbero mancare a seconda della tua configurazione. C’è chi ne usa di più e chi di meno (YMMV).

Nota che abbiamo due mappature di volumi che puntano al contenitore: la prima è per PostgreSQL, configurata come volume persistente che sopravviverà ai riavvii dei pod. La seconda è una mappatura di configurazione creata in questo modo:

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

Dove kotlin-forum-bootstrap deve corrispondere alla tua configurazione Kubernetes e path/to/bootstrap.yml è il percorso del file bootstrap.yml creato sopra utilizzando launcher dump.

Una volta che il tuo configmap è in posizione, dovresti essere in grado di scalare la tua distribuzione a una replica e vedere Discourse avviarsi ed eseguire lo stesso processo di avvio che launcher bootstrap avrebbe eseguito. Ci vogliono alcuni minuti. Quando questo è completato, la tua installazione di Discourse si avvierà.

Altri dettagli di configurazione

Alcune altre note che ho incontrato lungo la strada per ottenere questa configurazione (almeno per ora) completamente funzionante:

  • Qualsiasi proxy a monte deve inoltrare le intestazioni X-Forwarded, inclusi sia X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Port. Non farlo comporterà strani errori di autenticazione quando si tenta di utilizzare l’accesso con Google e probabilmente altri provider di accesso.
  • Il tuo controller ingress nginx deve essere configurato per passare le intestazioni impostando use-forwarded-headers nella configurazione globale del configmap. Ci è voluto un po’ per farlo funzionare correttamente, poiché almeno diverse volte ho modificato il configmap sbagliato e poi mi aspettavo che i miei container ingress si riavviassero quando il configmap cambiava. (Non lo hanno fatto.)

Aggiornamento

Per aggiornare l’installazione distribuita, rigeneri il nuovo file bootstrap.yml, aggiorni il configmap e poi riavvii il contenitore (più facilmente scalando a 0 e poi di nuovo a 1 replica).

Questo comporta un po’ di downtime poiché l’avvio avviene prima che il contenitore sia costruito. Ma questo mi sembra inevitabile nei casi in cui è necessario aggiornare la configurazione e/o cambiare l’immagine base. launcher rebuild è documentato come stop; bootstrap; start, il che significa che il processo di avvio causerà comunque downtime anche se eseguito utilizzando lo script launcher.

Commenti

Questo modello di distribuzione monolitica di Discourse sarebbe molto più semplice da supportare se lo script launcher separasse più chiaramente (a) i passaggi di avvio che potrebbero essere eseguiti offline e influenzare solo i file nel contenitore e (b) i passaggi di avvio che modificano o necessitano accesso al database o ad altro stato esterno al contenitore. L’approccio descritto sopra è un po’ frustrante perché vedi tutti i tipi di offuscamento JS, minimizzazione delle risorse e altre cose che potrebbero essere fatte con la distribuzione precedente in esecuzione… ma sono semplicemente troppo mescolate con altre cose (come le migrazioni del database) che non possono essere eseguite senza accesso al database. Ho pensato brevemente di creare un contenitore che eseguisse solo i passaggi in templates/postgres.yml, ma poi ho notato che le migrazioni del database venivano eseguite dal template web, ho pensato ai plugin e poi ho semplicemente rinunciato :slight_smile:.

Con una migliore separazione, la ridistribuzione per i contenitori monolitici potrebbe funzionare in questo modo:

  • Costruire un nuovo contenitore monolitico offline eseguendo tutti i passaggi interni al contenitore
  • Pubblicare quel contenitore
  • Quando si è pronti per l’aggiornamento, fermare il contenitore precedente, avviare il nuovo contenitore monolitico e lasciarlo completare eventuali passaggi che necessitano accesso al database. In base ai miei esperimenti, questi sembrano più veloci di alcuni degli altri passaggi di avvio.

Questo comporterebbe un po’ meno downtime. Probabilmente non vale la pena del solo sforzo per questo motivo, ma posso immaginare che questo potrebbe anche semplificare scenari di distribuzione più complessi che coinvolgono database condivisi o altro.

Ha più senso. È quello che ho fatto quando ho separato in due fasi la fase di avvio (bootstrap). La prima può essere eseguita in un ambiente isolato (come una pipeline CI) generando un’immagine di base con il repository di Discourse, le gemme e i plugin installati, mentre la seconda fase deve essere eseguita sulla macchina di destinazione (o quantomeno avere accesso al database di produzione) per eseguire la migrazione del database e generare le risorse (questo avviene nel processo di avvio, non all’avvio del contenitore).

Sì, sarebbe fantastico. L’ho già richiesto, ma non so se e quando verrà implementato.

Sarebbe difficile implementarlo completamente in un ambiente separato perché l’attività di precompilazione delle risorse necessita di accesso al database (per cose come il CSS personalizzato), ma sarebbe ottimo se solo ciò che dipende dal database potesse essere fatto in una fase separata (e tutte le altre risorse, che non dipendono dal database, potessero essere precompilate separatamente, anche se non so quanto sia tecnicamente realizzabile).

È esattamente quello che faccio negli installazioni Kubernetes che ho realizzato. Non riesco a immaginare come o perché usare k8s senza container separati per i dati e per il web (o qualche altro tipo di database Postgres e Redis esterno: le installazioni che ho fatto per i clienti utilizzano risorse GCP per questo).

Inoltre, esiste una variabile d’ambiente skip_post_migration_updates che è necessario comprendere per eseguire aggiornamenti con zero tempi di inattività. È descritta qui.