Archivos locales al trasladar una aplicación a Kubernetes

Archivos locales al trasladar una aplicación a Kubernetes

Al construir el proceso de CI/CD utilizando Kubernetes, a veces surge el problema de la incompatibilidad entre los requisitos de la nueva infraestructura y la aplicación que se está trasladando a ella. En particular, en la etapa de construcción de la aplicación, es importante obtener uno una imagen que se utilizará en todas los entornos y clústeres del proyecto. Este principio es la base de una correcta según Google gestión de contenedores (esto ya lo ha mencionado decía nuestro director técnico).

Sin embargo, no es raro ver situaciones en las que el código del sitio utiliza un framework listo para usar, cuyo uso impone restricciones sobre su futura explotación. Y si en un 'entorno normal' esto se puede manejar fácilmente, en Kubernetes un comportamiento así puede convertirse en un problema, especialmente cuando te enfrentas a ello por primera vez. Aunque una mente ingeniosa puede ofrecer soluciones de infraestructura que parecen obvias e incluso buenas a primera vista... es importante recordar que la mayoría de las situaciones pueden y deben ser tratadas arquitectónicamente..

Analizaremos soluciones de workaround populares para el almacenamiento de archivos, que pueden llevar a consecuencias desagradables durante la explotación del clúster, y también señalaremos un camino más adecuado.

Almacenamiento de estáticos

Para ilustrar, consideremos una aplicación web que utiliza un generador de estáticos para obtener un conjunto de imágenes, estilos y otros. Por ejemplo, en el framework PHP Yii hay un gestor de activos integrado que genera nombres de directorios únicos. En consecuencia, al final obtenemos un conjunto de rutas que no se cruzan entre sí para los estáticos del sitio (esto se hace por varias razones, como para evitar duplicados al usar el mismo recurso en múltiples componentes). Así, de forma predeterminada, al primer acceso al módulo del recurso web, se genera y distribuye la estática (en realidad, a menudo son symlinks, pero hablaremos de esto más adelante) con un catálogo raíz común único para este despliegue:

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

¿Cuáles son las implicaciones de esto en el contexto del clúster?

Un ejemplo simple

Tomemos un caso bastante común, donde nginx se encuentra delante de PHP para servir estáticos y procesar solicitudes simples. La forma más sencilla es Deployment con dos contenedores:

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

De manera simplificada, la configuración de nginx se reduce a lo siguiente:

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;
        }
    }

La primera vez que se accede al sitio, los activos aparecen en el contenedor con PHP. Sin embargo, en el caso de dos contenedores dentro de un solo pod, nginx no conoce estos archivos estáticos que (según la configuración) deben ser entregados precisamente por él. Como resultado, para todas las solicitudes a archivos CSS y JS, el cliente verá un error 404. La solución más simple aquí sería organizar un directorio común para los contenedores. Una opción primitiva es un directorio compartido. 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

Ahora los archivos estáticos generados en el contenedor se entregan correctamente por nginx. Pero recuerdo que esta es una solución primitiva, así que está lejos de ser ideal y tiene sus matices y fallos, que se detallan a continuación.

Almacenamiento más avanzado

Ahora imaginemos una situación en la que un usuario accede al sitio, carga una página con los estilos existentes en el contenedor, y mientras lee esa página, hemos desplegado nuevamente el contenedor. En el directorio de activos se ha quedado vacío y se requiere una solicitud a PHP para iniciar la generación de nuevos. Sin embargo, incluso después de esto, los enlaces a la antigua estática no serán válidos, lo que llevará a errores en la visualización de la estática.

Además, probablemente tengamos un proyecto más o menos cargado, lo que significa que una sola copia de la aplicación no será suficiente:

  • Escalaremos Deployment a dos réplicas.
  • En la primera solicitud al sitio, se crearon activos en una réplica.
  • En algún momento, el ingress decidió (con el fin de balancear la carga) enviar la solicitud a la segunda réplica, y allí esos activos aún no están. O puede que ya no estén, porque estamos usando RollingUpdate y actualmente estamos haciendo el despliegue.

En resumen, el resultado son nuevamente errores.

Para no perder los antiguos activos, se puede cambiar emptyDir en hostPath, almacenando la estática físicamente en un nodo del clúster. Este enfoque es malo porque en realidad tenemos que vincular nuestra aplicación a un nodo específico del clúster ya que, en caso de movernos a otros nodos, el directorio no contendrá los archivos necesarios. O se requeriría una sincronización de fondo del directorio entre nodos.

¿Cuáles son las posibles soluciones?

  1. Si el hardware y los recursos lo permiten, se puede usar cephfs para organizar un directorio accesible de manera uniforme para las necesidades de la estática. Documentación oficial se recomienda discos SSD, al menos una replicación triple y una conexión 'gorda' estable entre los nodos del clúster.
  2. Una opción menos exigente sería organizar un servidor NFS. Sin embargo, entonces hay que tener en cuenta el posible aumento del tiempo de respuesta en el procesamiento de solicitudes por parte del servidor web, y la resistencia a fallos dejará mucho que desear. Las consecuencias de una falla son catastróficas: la pérdida de un mount condena al clúster a la muerte bajo la presión de la carga LA, que se dispara al cielo.

Además de todo lo anterior, para todas las opciones de creación de almacenamiento permanente se requerirá una limpieza de fondo de conjuntos de archivos obsoletos acumulados durante un cierto período de tiempo. Delante de los contenedores con PHP se puede colocar un DaemonSet de nginx en caché, que almacenará copias de los activos durante un tiempo limitado. Este comportamiento se puede configurar fácilmente mediante proxy_cache con la profundidad de almacenamiento en días o gigabytes de espacio en disco.

Combinar este método con los sistemas de archivo distribuidos mencionados anteriormente abre un enorme campo de posibilidades, con limitaciones solo en el presupuesto y en el potencial técnico de quienes lo implementen y mantengan. Por experiencia, diremos que cuanto más simple sea el sistema, más estable funcionará. Al añadir capas como estas, mantener la infraestructura se vuelve mucho más complicado, y con ello aumenta el tiempo dedicado al diagnóstico y recuperación en caso de fallos.

Recomendación

Si la implementación de las opciones de almacenamiento propuestas también le parece injustificada (compleja, costosa...), vale la pena mirar la situación desde otro ángulo. Es decir, profundizar en la arquitectura del proyecto y erradicar el problema en el código, vinculándose a alguna estructura de datos estática en la imagen, con una definición clara del contenido o del procedimiento de "calentamiento" y/o precompilación de los activos en la etapa de construcción de la imagen. Así, obtenemos un comportamiento absolutamente predecible y un conjunto idéntico de archivos para todos los entornos y réplicas de la aplicación en ejecución.

Si volvemos al ejemplo concreto con el framework Yii y no profundizamos en su estructura (lo cual no es el objetivo del artículo), basta con señalar dos enfoques populares:

  1. Cambiar el proceso de construcción de la imagen para colocar los activos en un lugar predecible. Así lo sugieren/implementan en extensiones como yii2-static-assets.
  2. Definir hashes específicos para los directorios de activos, tal como se explica, por ejemplo, en esta presentación (empezando desde la diapositiva №35). Por cierto, el autor de la presentación finalmente (¡y no sin razones!) aconseja después de construir los activos en el servidor de construcción, cargarlos en un almacenamiento central (como S3), delante del cual colocar un CDN.

Archivos subidos

Otro caso que seguramente será útil al trasladar la aplicación a un clúster de Kubernetes es el almacenamiento de archivos de usuario en el sistema de archivos. Por ejemplo, tenemos nuevamente una aplicación en PHP que recibe archivos a través de un formulario de carga, realiza ciertas acciones con ellos durante su ejecución y los devuelve.

El lugar donde estos archivos deben colocarse, en el contexto de Kubernetes, debe ser compartido para todas las réplicas de la aplicación. Dependiendo de la complejidad de la aplicación y de la necesidad de organizar la persistencia de estos archivos, ese lugar puede ser las opciones mencionadas anteriormente de dispositivos compartidos, pero, como vemos, tienen sus desventajas.

Recomendación

Una de las soluciones es utilizar un almacenamiento compatible con S3 (aunque sea algún tipo de categoría auto-alojada como minio). Cambiar a trabajar con S3 requerirá ajustes a nivel de código, y cómo se entregará el contenido en el front-end, ya hemos hemos escrito.

Sesiones de usuario

Es importante señalar por separado la organización del almacenamiento de sesiones de usuario. A menudo, esto también implica archivos en disco, lo que en el ámbito de Kubernetes puede resultar en constantes solicitudes de autorización al usuario si su solicitud se dirige a otro contenedor.

En parte, el problema se resuelve activando stickySessions en el ingress (esta característica es compatible con todos los controladores de ingress populares; más detalles en resumen), para vincular al usuario a un pod específico de la aplicación:

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: /

Pero esto no eliminará los problemas en despliegues posteriores.

Recomendación

Un enfoque más adecuado sería trasladar la aplicación a almacenamiento de sesiones en memcached, Redis y soluciones similares — en general, deshacerse por completo de las opciones basadas en archivos.

Conclusión

Las soluciones de infraestructura consideradas en el texto solo merecen ser aplicadas como «muletas» temporales (lo cual suena más elegantemente en inglés como workaround). Son relevantes en las primeras etapas de la migración de la aplicación a Kubernetes, pero no deben «echar raíces».

El camino recomendado es deshacerse de ellas a favor de una modificación arquitectónica de la aplicación de acuerdo con el ya bien conocido 12-Factor App. Sin embargo, llevar la aplicación a un estado sin estado inevitablemente requerirá cambios en el código, y aquí es importante encontrar un equilibrio entre las capacidades/requerimientos del negocio y las perspectivas de implementación y mantenimiento del camino elegido.

P.D.

También puedes leer en nuestro blog:

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster