Kubernetes - Migration d'une installation Nextcloud vers Kubernetes (RaspBerry Pi/Rock64)

Cette partie va consister à migrer mon serveur Nextcloud existant vers le cluster Kubernetes (k8s).

La première chose à vérifier : Existe-t-il un container officiel Nextcloud pour les plateformes “Arm64” ?

Le dépôt existe bien, maintenu par la communauté Nextcloud (officiel) et disponible pour Arm64 (Docker Registry).

Contexte et Architecture

Architecture

  • Cluster Kubernetes (ARM64)
  • Serveur GlusterFS (ARM64)
  • Serveur de Base de données Postgres (ARM64)

Contexte

J’ai déjà une installation nextcloud sur mon Datacenter, répartie sur un serveur Postgres, un serveur web Apache/PhP et les données NextCloud sur un disque SSD.

ancienne architecture

Les données Nextcloud seront migrées sur le serveur GlusterFS (voir installer un serveur GlusterFS. Les données Postgres restent sur le même serveur :

nouvelle architecture

Il est impératif de faire une sauvegarde de tout l’ensemble Nextcloud avant de migrer vers Kubernetes.

Préparation du déploiement

Nous allons préparer le déploiement de Nextcloud dans Kubernetes fonction des spécifications fournies par la documentation relative au conteneur “Nextcloud-Docker”. Rappel : je pars d’une configuration de production déjà existante

A l’heure où j’écris cette documentation, les spécifications de “volumes” pour Nextcloud sont :

  • /var/www/html Main folder, needed for updating
  • /var/www/html/custom_apps installed / modified apps
  • /var/www/html/config local configuration
  • /var/www/html/data the actual data of your Nextcloud
  • /var/www/html/themes/<YOUR_CUSTOM_THEME> theming/branding

Mon service est concerné par : config , custom_apps et data, je n’utilise pas de thèmes spécifiques.

Préparation des volumes

Serveur GlusterFS

je vais donc créer trois volumes GlusterFS :

  • nextcloud-config
  • nextcloud-data
  • nextcloud-apps

Sur le serveur GlusterFS, mes volumes seront stockés sous “/Disk1/gfs-nextcloud”, je créé et démarre les volumes gluster nécessaires :

BASEGFS="/Disk1/gfs-nextcloud"
mkdir $BASEGFS
VOLUMES="config data apps"
for VOLUME in $VOLUMES
do
  # Create dir
  mkdir $BASEGFS/$VOLUME
  # Create volume
  gluster volume create nextcloud-$VOLUME  $(hostname):/$BASEGFS/$VOLUME
  # Start volume
  gluster volume start nextcloud-$VOLUME
done

Firewall du serveur Gluster : Ajouter un volume au domaine Gluster, veut aussi dire modifier les règles de firewall du “Gluster Server” (1 volume exporté = 1 nouveau port), voir la doc installer un serveur GlusterFS

Serveur Nextcloud (celui qui dispose des données à migrer)

Je monte les volumes gluster, ceci permet de vérifier le bon fonctionnement du serveur Gluster : mount -t glusterfs [gluster host]:/[volume name], par exemple :

# Mount du volume nextcloud-config sur /mnt du serveur Nextcloud, vous devrez peut etre au préalable, installer le client gluster : `apt -y install glusterfs-client`
mount -t glusterfs gluster.mydomaine:/nextcloud-config /mnt
# Pour démonter 
umount /mnt

Une fois monté, et selon le volume, copiez le contenu nécessaire, répéter l’opération pour chacun des volume :

  • le contenu de “config” de l’installation nextcloud existante dans le volume Gluster : nextcloud-config
  • le contenu de “data” de l’installation nextcloud existante dans le volume Gluster : nextcloud-data
  • le contenu de “apps” de l’installation nextcloud existante, sans les apps livrées par défaut, dans le volume Gluster : nextcloud-apps

ATTENTION pour le volume apps : Voici la liste des apps nextcloud (core) présentes par défaut :

LIST="accessibility activity admin_audit cloud_federation_api comments contactsinteraction dashboard dav encryption federatedfilesharing federation files files_external files_pdfviewer files_rightclick files_sharing files_trashbin files_versions files_videoplayer firstrunwizard logreader lookup_server_connector nextcloud_announcements notifications oauth2 password_policy photos privacy provisioning_api recommendations serverinfo settings sharebymail support survey_client systemtags text theming twofactor_backupcodes updatenotification user_ldap user_status viewer weather_status workflowengine"

ATTENTION Cette liste peut changer à tous moments selon la version que vous migrez… Le plus simple est de copier le contenu de apps dans le volume “nextcloud-apps” et de supprimer les répertoires mentionnés ci-avant.

LIST="[valeur précédemment citée"
cd [Votre volume gluster]
for rep in $LIST
do
  rm -rf "$rep"
done

ATTENTION Si vous copier les données directement dans le répertoire du serveur Gluster, sans passer par un client Gluster, vous serez confrontés à un problème de synchronisation entre ce qui est disposé sur le serveur gluster et ce que “voit” les clients. Voir la solution à la fin de cette procédure

Automatisation

Votre service nextcloud actif est installé dans “/var/www/nextcloud”, le noms des volumes Gluster est conforme à ceux précités, rsync et le client glusterfs sont installés :

Sur le serveur nextcloud actif Nous allons procéder au transfert des données active vers le serveur GlusterFS :

# Arret du service
systemctl stop apache2
NEXTCLOUDPLACE="/var/www/nextcloud"
HOSTNAMEGLUSTER="gluster.server" # ou adresse IP du serveur gluster
VOLUMES="config data apps"
# Pour etre sûr 
umount /mnt
for V in $VOLUMES
do
  mount -t glusterfs "$HOSTNAMEGLUSTER:/nextcloud-$V /mnt
  rsync -ravH "$NEXTCLOUDPLACE/$V/ /mnt/
  ## Suppression des répertoires apps "non custom"
  if [ "$V" == "apps" ];then
    LIST="accessibility activity admin_audit cloud_federation_api comments contactsinteraction dashboard dav encryption federatedfilesharing federation files files_external files_pdfviewer files_rightclick files_sharing files_trashbin files_versions files_videoplayer firstrunwizard logreader lookup_server_connector nextcloud_announcements notifications oauth2 password_policy photos privacy provisioning_api recommendations serverinfo settings sharebymail support survey_client systemtags text theming twofactor_backupcodes updatenotification user_ldap user_status viewer weather_status workflowengine"
    for rep in $LIST
    do
      rm -rf "/mnt/$rep"
    done
  fi
  umount /mnt
done

La durée de l’opération dépend de la volumétrie à transférer

Ajustement de la configuration Nextcloud

La configuration de nextcloud “config.php” de l’installation existante est maintenant copiée dans le volume GlusterFs “nextcloud-config”. Editer ce fichier en remplaçant la valeur de la propriété ‘datadirectory’ par “/var/www/html/data” qui est la configuration standard de l’image Docker fabriquée par Nextcloud :

<?php
$CONFIG = array (
[...]
  'datadirectory' => "/var/www/html/data"
[...]

Base de données

Rien à faire dans mon cas, la base reste en place sur le serveur de base de données actuel. Je rappelle que mon serveur Nextcloud n’héberge pas la base de données “postgres”, elle est déjà disposé sur un autre serveur.

Préparation du serveur de base de données Postgres

Le firewall du serveur et le fichier pg_hba.conf doivent autoriser les accès à tous les “Workers” du cluster Kubernetes. Pour postgres il s’agit généralement du port 5432 qui doit diposer d’une règle : INPUT Allow 5432/TCP (toutes les adresse IP des workers). Pour le pg_hba.conf, vous avez limité l’accès à la base Nextcloud à une seule machine, pour indiquer plusieurs machines agir sur le masque de sous-réseau (subnet.ninja-subnet-cheat-sheet), ou bien ajouter autant de lignes que nécessaire :

  • un réseau complet - - Attention ceci élargit la surface d’attaque
host nextcloud nextcloud 172.28.4.0/24 md5

ou bien autant de lignes que de hosts autorisés à joindre la base de données (meilleur choix), exemple ici avec 3 workers

[...]
host nextcloud nextcloud 172.28.4.72/32 md5
host nextcloud nextcloud 172.28.4.73/32 md5
host nextcloud nextcloud 172.28.4.74/32 md5
[...]

Pour appliquer les modifications, ne redémarrez pas le service Postgres (restart) pour ne pas “couper la production”, utilisez le paramètre “reload” : … systemctl reload postgresql …

Création du manifest

Le manifest sera composé de :

  • 1 namespace : nextcloud
  • 1 service gluster
  • 1 endpoint gluster
  • 3 Volumes persistants : Il s’agit des accès à nos 3 volumes Glusterfs
  • 1 deployment
  • 1 service réseau permettant l’accès à l’application nextcloud de type : NodePort

Fichier à créer : “manifest-nextcloud.yaml” sur le master (commande kubectl), Adapter les valeurs :

  • [adresse IP du serveur glusterfs]
  • [Nom du volume gluster fs pour les données nextcloud]
  • [Nom du volume gluster fs pour les apps nextcloud]
  • [Nom du volume gluster fs pour la config nextcloud]
apiVersion: "v1"
kind: "Namespace"
metadata: 
  name: "nextcloud"
  labels: 
    name: "nextcloud"
---
apiVersion: "v1"
kind: "Service"
metadata:
  name: "glusterfs-cluster"
  namespace: "nextcloud"
spec:
  ports:
  - port: 1
---
apiVersion: v1
kind: "Endpoints"
metadata:
  name: "glusterfs-cluster"
  namespace: "nextcloud"
subsets:
- addresses:
  - ip: [adresse IP du serveur glusterfs]
  ports:
  - port: 1
---
apiVersion: "v1"
kind: "PersistentVolume"
metadata:
  name: "gluster-pv-[Nom du volume gluster fs pour les données nextcloud]"
  namespace: "nextcloud"
spec:
  capacity:
    storage: "400Gi"
  accessModes:
    - "ReadWriteMany"
  storageClassName: "[Nom du volume gluster fs pour les données nextcloud]"
  persistentVolumeReclaimPolicy: "Recycle"
  glusterfs:
    endpoints: "glusterfs-cluster"
    path: "[Nom du volume gluster fs pour les données nextcloud]"
    readOnly: false
---
apiVersion: "v1"
kind: "PersistentVolume"
metadata:
  name: "gluster-pv-[Nom du volume gluster fs pour les apps nextcloud]"
  namespace: "nextcloud"
spec:
  capacity:
    storage: "1Gi"
  accessModes:
    - "ReadWriteMany"
  storageClassName: "[Nom du volume gluster fs pour les apps nextcloud]"
  persistentVolumeReclaimPolicy: "Recycle"
  glusterfs:
    endpoints: "glusterfs-cluster"
    path: "[Nom du volume gluster fs pour les apps nextcloud]"
    readOnly: false
---
apiVersion: "v1"
kind: "PersistentVolume"
metadata:
  name: "gluster-pv-[Nom du volume gluster fs pour la config nextcloud]"
  namespace: "nextcloud"
spec:
  capacity:
    storage: "4k"
  accessModes:
    - "ReadWriteMany"
  storageClassName: "[Nom du volume gluster fs pour la config nextcloud]"
  persistentVolumeReclaimPolicy: "Recycle"
  glusterfs:
    endpoints: "glusterfs-cluster"
    path: "[Nom du volume gluster fs pour la config nextcloud]"
    readOnly: false
---
apiVersion: "v1"
kind: "PersistentVolumeClaim"
metadata:
  name: "glusterfs-claim-[Nom du volume gluster fs pour les données nextcloud]"
  namespace: "nextcloud"
spec:
  accessModes:
    - "ReadWriteMany"
  storageClassName: "[Nom du volume gluster fs pour les données nextcloud]"
  resources:
    requests:
      storage: "400Gi"
---
apiVersion: "v1"
kind: "PersistentVolumeClaim"
metadata:
  name: "glusterfs-claim-[Nom du volume gluster fs pour les apps nextcloud]"
  namespace: "nextcloud"
spec:
  accessModes:
    - "ReadWriteMany"
  storageClassName: "[Nom du volume gluster fs pour les apps nextcloud]"
  resources:
    requests:
      storage: "1Gi"
---
apiVersion: "v1"
kind: "PersistentVolumeClaim"
metadata:
  name: "glusterfs-claim-[Nom du volume gluster fs pour la config nextcloud]"
  namespace: "nextcloud"
spec:
  accessModes:
    - "ReadWriteMany"
  storageClassName: "[Nom du volume gluster fs pour la config nextcloud]"
  resources:
    requests:
      storage: "4k"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: "nextcloud"
  namespace: "nextcloud"
spec:
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nextcloud
  replicas: 1 
  template: 
    metadata:
      labels:
        app: nextcloud
    spec:
      containers:
      - name: nextcloud
        image: nextcloud
        ports:
          - containerPort: 80
        volumeMounts:
          - mountPath: "/var/www/html/config"
            name: glusterfsvol-config
          - mountPath: "/var/www/html/data"
            name: glusterfsvol-data   
          - mountPath: "/var/www/html/custom_apps"
            name: glusterfsvol-apps   
      volumes:
        - name: glusterfsvol-config
          persistentVolumeClaim:
            claimName: glusterfs-claim-[Nom du volume gluster fs pour la config nextcloud]
        - name: glusterfsvol-data
          persistentVolumeClaim:
            claimName: glusterfs-claim-[Nom du volume gluster fs pour les données nextcloud]
        - name: glusterfsvol-apps
          persistentVolumeClaim:
            claimName: glusterfs-claim-[Nom du volume gluster fs pour les apps nextcloud]
---
apiVersion: v1
kind: Service
metadata:
  name: "nextcloud"
  namespace: "nextcloud"
spec:
  type: NodePort 
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: "nextcloud"

Appliquer le manifest

kubectl apply -f nextcloud-deployment.yaml
# deployment.apps/nextcloud created

Vérifiez que le container démarre : (Je vous conseille de relire la documentation d’installation d’un cluster si vous ne disposez pas de la complétion automatique “kubectl”

kubectl get pod -n nextcloud <touche tab>
# La fin du log indique l'état du pod, attendre quelques minutes pour un démarrage complet...
# répéter la commande précédente, jusqu'au démarrage complet : READY 1/1
NAME                         READY   STATUS    RESTARTS   AGE
nextcloud-6759b5df47-blqbk   1/1     Running   0          5d23h

Exposition

Notez que le manifest inclut le “service nextcloud” et permet d’exposer cette application à l’extérieur du cluster (type NodePort).

Pour connaître ce port :

kubectl get service -n nextcloud nextcloud
NAME                TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
nextcloud           NodePort    10.107.203.125   <none>        80:31812/TCP   63d

Pour atteindre ce nouveau service voir la procédure Exposer les services avec un serveur HAPROXY

Mettre à l’échelle NextCloud (Horizontal scaling)

Concernant l’étape de mise à l’échelle (ici horizontal scaling, c’est à dire deux serveurs web qui utilisent les mêmes ressources), il faut un service Redis et apporter quelques modifications à la configuration du “deployment K8s”. Nextcloud à choisi de stocker les sessions dans un service Redis, plus rapide qu’une implémentation dans la base de données Postgres.

Installation d’un serveur Redis

J’ai choisi de l’installer sur le serveur de base de données, peu chargé, voir la documentation:

Mise en place de deux replicas sur deux workers différents

Ici, je vais ajouter la notion de replicas, c’est à dire le nombre de pods nécessaire à la bonne exécution de votre plan. Pour ma part, je vais répartir sur deux workers. On va donc aussi voir la notion de NodeSelector.

Pour appliquer ces modifications :

  • Vous repartez du manifest créé précédemment puis exécuter la commande :
kubectl apply -f nextcloud-deployment.yaml

OU bien vous modifiez directement le “deployment” avec le paramètre “edit” de kubectl :

kubectl edit deployments.apps -n nextcloud nextcloud
[ESC:wq]
# Kube applique les modifications immédiatement ou bien renvoie une erreur
Notion de replicas

Fichier manifest : node /spec (root), ajoutez “replicas:[nombre de replique]”

[...]
spec:
    replicas: 2
[...]
Notion affinity (NodeSelector)

Attribut node spec/template/spec ajout de l’affinité qui permet de démarrer un pod sur un serveur déjà ‘équipé’ du container démarré :

    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - nextcloud
            topologyKey: kubernetes.io/hostname
[...]
Partie Nexcloud

Ces informations sont issues de la documentation Nextcloud. Pour démarrer un container Nextcloud devant utiliser un stockage des sessions, Nextcloud utilise le service Redis. Vous devez donc fournir, au moyens de variables d’environnement, les informations nécessaires à la connexion au service Redis. Par défaut, Nextcloud stocke les sessions sous forme de fichiers et localement (donc dans chaque conteneur). Quand vous souhaitez mettre à l’échelle, nextcloud utilise Redis comme seul point de stockage des sessions. Ceci permet de rester authentifié lorsque le load balancer décide de vous diriger vers un serveur, autre que celui qui vous a fournit la session d’authentification.

Fichier manifest : node spec/template/spec/containers

[...]
      containers:
      - env:
        - name: REDIS_HOST
          value: [adresse IP ou FDQN du serveur Redis]
        - name: REDIS_HOST_PORT
          value: "6379"
        - name: REDIS_HOST_PASSWORD
          value: [Mot de passe d'accès au service Redis]
[...]

Attention j’ai fait simple : la bonne pratique empêche le stockage de mots de passe en “clair” dans les configurations, pour rendre la configuration plus sécurisée, voir la notion de “secrets Kubernetes”.

Après application des modifications, les deux pods démarrent bien sur deux workers différents, le serveur Haproxy connait déjà les deux nodes, il ne reste plus qu’à vérifier, sur le serveur Haproxy, que le traffic passe bien sur les deux serveurs (ma config haproxy spécifie un équilibrage de type “roundrobin”).

Niveau de version de l’image Docker nextcloud

Si vous scaler horizontalement cette application, changer le nom de l’image Docker, en indiquant un numéro de version précis, par exemple : nextcloud:20.0.7. La configuration que j’ai donné ci-avant, permet de vérifier qu’il n’existe pas une version plus récente dans le dépôt Docker Hub. C’est pratique quand vous disposez d’un seul POD. Lorsque nextcloud prévient d’une évolution de version, il suffit de stopper le POD, K8s le redémarre automatiquement avec la nouvelle version de Nextcloud. Lors du redémarrage d’un des conteneurs nextcloud, le serveur worker ira chercher la dernière version disponible sur le repository DockerHub, et dans le cas où vous avez des réplicas, les autres containers continueront d’utiliser l’ancienne version.
Vous serez très rapidement confrontés à un conflit de version entre les conteneurs en cours d’exécution, avec une demande d’évolution d’un côté, puis d’un “downgrade” de l’autre, juste après que le nouveau conteneur a exécuté “l’upgrade” !!!

Problèmes rencontrés

Glusterfs

  • Le point de montage gluster ne fait apparaître qu’une partie des fichiers attendus : vous n’avez pas copié les fichiers au travers d’un client Glusterfs La solution est ici