Fichiers locaux lors de la migration d'une application vers Kubernetes

Fichiers locaux lors de la migration d'une application vers Kubernetes

Lors de la création d'un processus CI/CD à l'aide de Kubernetes, le problème d'incompatibilité entre les exigences de la nouvelle infrastructure et l'application qui y est transférée se pose parfois. En particulier, au stade de la création de l'application, il est important d'obtenir un image qui sera utilisée dans tous environnements de projet et clusters. Ce principe sous-tend le bon selon Google gestion des conteneurs (plus d'une fois à ce sujet dit et notre service technique).

Cependant, vous ne verrez personne dans les situations où le code du site utilise un cadre prêt à l'emploi, dont l'utilisation impose des restrictions sur son utilisation ultérieure. Et même si dans un « environnement normal » cela est facile à gérer, dans Kubernetes, ce comportement peut devenir un problème, surtout lorsque vous le rencontrez pour la première fois. Même si un esprit inventif peut proposer des solutions d'infrastructure qui semblent évidentes, voire bonnes à première vue... il est important de se rappeler que la plupart des situations peuvent et doivent être résolu architecturalement.

Examinons les solutions de contournement populaires pour le stockage de fichiers qui peuvent entraîner des conséquences désagréables lors du fonctionnement d'un cluster, et indiquons également un chemin plus correct.

Stockage statique

Pour illustrer, considérons une application Web qui utilise une sorte de générateur statique pour obtenir un ensemble d'images, de styles et d'autres éléments. Par exemple, le framework Yii PHP dispose d'un gestionnaire d'actifs intégré qui génère des noms de répertoires uniques. En conséquence, le résultat est un ensemble de chemins pour le site statique qui ne se croisent évidemment pas (cela a été fait pour plusieurs raisons - par exemple, pour éliminer les doublons lorsque plusieurs composants utilisent la même ressource). Ainsi, dès la première fois que vous accédez à un module de ressources Web, des fichiers statiques (en fait, souvent des liens symboliques, mais nous y reviendrons plus tard) sont formés et disposés avec un répertoire racine commun unique pour ce déploiement :

  • webroot/assets/2072c2df/css/…
  • webroot/assets/2072c2df/images/…
  • webroot/assets/2072c2df/js/…

Qu’est-ce que cela signifie en termes de cluster ?

Exemple le plus simple

Prenons un cas assez courant, où PHP est précédé de nginx pour distribuer des données statiques et traiter des requêtes simples. Le moyen le plus simple - Déploiement avec deux conteneurs :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: site
spec:
  selector:
    matchLabels:
      component: backend
  template:
    metadata:
      labels:
        component: backend
    spec:
      volumes:
        - name: nginx-config
          configMap:
            name: nginx-configmap
      containers:
      - name: php
        image: own-image-with-php-backend:v1.0
        command: ["/usr/local/sbin/php-fpm","-F"]
        workingDir: /var/www
      - name: nginx
        image: nginx:1.16.0
        command: ["/usr/sbin/nginx", "-g", "daemon off;"]
        volumeMounts:
        - name: nginx-config
          mountPath: /etc/nginx/conf.d/default.conf
          subPath: nginx.conf

Sous une forme simplifiée, la configuration de nginx se résume à ce qui suit :

apiVersion: v1
kind: ConfigMap
metadata:
  name: "nginx-configmap"
data:
  nginx.conf: |
    server {
        listen 80;
        server_name _;
        charset utf-8;
        root  /var/www;

        access_log /dev/stdout;
        error_log /dev/stderr;

        location / {
            index index.php;
            try_files $uri $uri/ /index.php?$args;
        }

        location ~ .php$ {
            fastcgi_pass 127.0.0.1:9000;
            fastcgi_index index.php;
            include fastcgi_params;
        }
    }

Lorsque vous accédez pour la première fois au site, les ressources apparaissent dans le conteneur PHP. Mais dans le cas de deux conteneurs au sein d'un même pod, nginx ne sait rien de ces fichiers statiques, qui (selon la configuration) devraient leur être fournis. En conséquence, pour toutes les requêtes vers des fichiers CSS et JS, le client verra une erreur 404. La solution la plus simple ici serait d'organiser un répertoire commun pour les conteneurs. Option primitive - général emptyDir:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: site
spec:
  selector:
    matchLabels:
      component: backend
  template:
    metadata:
      labels:
        component: backend
    spec:
      volumes:
        - name: assets
          emptyDir: {}
        - name: nginx-config
          configMap:
            name: nginx-configmap
      containers:
      - name: php
        image: own-image-with-php-backend:v1.0
        command: ["/usr/local/sbin/php-fpm","-F"]
        workingDir: /var/www
        volumeMounts:
        - name: assets
          mountPath: /var/www/assets
      - name: nginx
        image: nginx:1.16.0
        command: ["/usr/sbin/nginx", "-g", "daemon off;"]
        volumeMounts:
        - name: assets
          mountPath: /var/www/assets
        - name: nginx-config
          mountPath: /etc/nginx/conf.d/default.conf
          subPath: nginx.conf

Désormais, les fichiers statiques générés dans le conteneur sont correctement servis par nginx. Mais permettez-moi de vous rappeler qu'il s'agit d'une solution primitive, ce qui signifie qu'elle est loin d'être idéale et qu'elle comporte ses propres nuances et inconvénients, qui sont discutés ci-dessous.

Stockage plus avancé

Imaginez maintenant une situation où un utilisateur visite le site, charge une page avec les styles disponibles dans le conteneur, et pendant qu'il lisait cette page, nous redéployons le conteneur. Le catalogue d'actifs est devenu vide et une requête vers PHP est nécessaire pour commencer à en générer de nouveaux. Cependant, même après cela, les liens vers les anciennes statistiques ne seront plus pertinents, ce qui entraînera des erreurs dans l'affichage des statistiques.

De plus, nous avons très probablement un projet plus ou moins chargé, ce qui signifie qu'une seule copie de l'application ne suffira pas :

  • Agrandissons-le Déploiement jusqu'à deux répliques.
  • Lors du premier accès au site, les actifs ont été créés dans une seule réplique.
  • À un moment donné, Ingress a décidé (à des fins d'équilibrage de charge) d'envoyer une requête au deuxième réplica, et ces actifs n'étaient pas encore là. Ou peut-être qu'ils ne sont plus là parce que nous utilisons RollingUpdate et en ce moment nous effectuons le déploiement.

En général, le résultat est encore une fois des erreurs.

Pour éviter de perdre d'anciens actifs, vous pouvez modifier emptyDir sur hostPath, ajoutant physiquement de la statique à un nœud de cluster. Cette approche est mauvaise car nous devons en réalité se lier à un nœud de cluster spécifique votre application, car - en cas de déplacement vers d'autres nœuds - le répertoire ne contiendra pas les fichiers nécessaires. Ou une sorte de synchronisation de répertoire en arrière-plan entre les nœuds est requise.

Quelles sont les solutions ?

  1. Si le matériel et les ressources le permettent, vous pouvez utiliser cephfs pour organiser un répertoire également accessible pour les besoins statiques. Documents officiels recommande les disques SSD, une réplication au moins triple et une connexion « épaisse » stable entre les nœuds du cluster.
  2. Une option moins exigeante serait d'organiser un serveur NFS. Cependant, vous devez alors prendre en compte l'augmentation possible du temps de réponse pour le traitement des requêtes par le serveur Web, et la tolérance aux pannes laissera beaucoup à désirer. Les conséquences d'un échec sont catastrophiques : la perte de la monture condamne le cluster à la mort sous la pression de la charge LA s'engouffrant dans le ciel.

Entre autres choses, toutes les options de création de stockage persistant nécessiteront nettoyage d'arrière-plan ensembles de fichiers obsolètes accumulés sur une certaine période de temps. Devant les conteneurs avec PHP vous pouvez mettre Ensemble de démons de la mise en cache de nginx, qui stockera des copies des actifs pendant une durée limitée. Ce comportement est facilement configurable en utilisant proxy_cache avec une profondeur de stockage en jours ou en gigaoctets d'espace disque.

La combinaison de cette méthode avec les systèmes de fichiers distribués mentionnés ci-dessus offre un immense champ d'imagination, limité uniquement par le budget et le potentiel technique de ceux qui la mettront en œuvre et la prendront en charge. Par expérience, nous pouvons dire que plus le système est simple, plus il fonctionne de manière stable. Lorsque de telles couches sont ajoutées, il devient beaucoup plus difficile de maintenir l'infrastructure, et en même temps, le temps consacré au diagnostic et à la récupération en cas de panne augmente.

Recommandation

Si la mise en œuvre des options de stockage proposées vous semble également injustifiée (compliquée, coûteuse...), alors cela vaut la peine de regarder la situation de l'autre côté. À savoir, approfondir l'architecture du projet et résoudre le problème dans le code, liée à une structure de données statique dans l'image, une définition sans ambiguïté du contenu ou une procédure de « préchauffage » et/ou de précompilation des actifs au stade de l'assemblage de l'image. De cette façon, nous obtenons un comportement absolument prévisible et le même ensemble de fichiers pour tous les environnements et répliques de l'application en cours d'exécution.

Si l’on revient à l’exemple précis du framework Yii et n’entre pas dans sa structure (ce qui n’est pas le but de l’article), il suffit de signaler deux approches populaires :

  1. Modifiez le processus de création d'image pour placer les ressources dans un emplacement prévisible. Ceci est suggéré/implémenté dans des extensions comme yii2-actifs-statiques.
  2. Définissez des hachages spécifiques pour les répertoires d'actifs, comme indiqué par ex. cette présentation (à partir de la diapositive n°35). D'ailleurs, l'auteur du rapport conseille finalement (et non sans raison !) d'après avoir assemblé les actifs sur le serveur de build, de les télécharger sur un stockage central (comme S3), devant lequel placer un CDN.

Téléchargements

Un autre cas qui entrera certainement en jeu lors de la migration d'une application vers un cluster Kubernetes est le stockage des fichiers utilisateur dans le système de fichiers. Par exemple, nous avons à nouveau une application PHP qui accepte les fichiers via un formulaire de téléchargement, en fait quelque chose pendant le fonctionnement et les renvoie.

Dans Kubernetes, l'emplacement où ces fichiers doivent être placés doit être commun à toutes les répliques de l'application. En fonction de la complexité de l'application et de la nécessité d'organiser la persistance de ces fichiers, les options de partage de périphériques mentionnées ci-dessus peuvent constituer un tel endroit, mais, comme nous le voyons, elles ont leurs inconvénients.

Recommandation

Une solution est en utilisant un stockage compatible S3 (même s'il s'agit d'une sorte de catégorie auto-hébergée comme minio). Le passage à S3 nécessitera des changements au niveau du code, et comment le contenu sera diffusé sur le front-end, nous l'avons déjà écrit.

Sessions utilisateur

Par ailleurs, il convient de noter l'organisation du stockage des sessions utilisateur. Il s'agit souvent également de fichiers sur disque, ce qui dans le contexte de Kubernetes entraînera des demandes d'autorisation constantes de la part de l'utilisateur si sa demande aboutit dans un autre conteneur.

Le problème est en partie résolu en allumant stickySessions à l'entrée (la fonctionnalité est prise en charge dans tous les contrôleurs d'entrée populaires - pour plus de détails, voir notre avis)pour lier l'utilisateur à un pod spécifique avec l'application :

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: nginx-test
  annotations:
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "route"
    nginx.ingress.kubernetes.io/session-cookie-expires: "172800"
    nginx.ingress.kubernetes.io/session-cookie-max-age: "172800"

spec:
  rules:
  - host: stickyingress.example.com
    http:
      paths:
      - backend:
          serviceName: http-svc
          servicePort: 80
        path: /

Mais cela n’éliminera pas les problèmes liés aux déploiements répétés.

Recommandation

Une manière plus correcte serait de transférer l'application vers stockage des sessions dans Memcached, Redis et solutions similaires - en général, abandonnez complètement les options de fichiers.

Conclusion

Les solutions d'infrastructure évoquées dans le texte ne valent la peine d'être utilisées que sous la forme de « béquilles » temporaires (ce qui sonne mieux en anglais comme solution de contournement). Ils peuvent être pertinents dans les premières étapes de migration d’une application vers Kubernetes, mais ne doivent pas prendre racine.

La voie générale recommandée est de s'en débarrasser au profit d'une modification architecturale de l'application conformément à ce qui est déjà bien connu de beaucoup. Application à 12 facteurs. Cependant, cela - amener l'application à une forme apatride - signifie inévitablement que des modifications du code seront nécessaires, et ici il est important de trouver un équilibre entre les capacités/exigences de l'entreprise et les perspectives de mise en œuvre et de maintien de la voie choisie. .

PS

A lire aussi sur notre blog :

Source: habr.com

Ajouter un commentaire