Pliki lokalne podczas przenoszenia aplikacji do Kubernetes

Pliki lokalne podczas przenoszenia aplikacji do Kubernetes

Podczas budowania procesu CI/CD z wykorzystaniem Kubernetesa czasami pojawia się problem niekompatybilności wymagań nowej infrastruktury z przenoszonym do niej aplikacją. W szczególności, na etapie budowy aplikacji istotne jest uzyskanie jeden obrazu, który będzie używany we wszystkich środowiskach i klastrach projektu. Ta zasada leży u podstaw prawidłowego zdaniem Google zarządzania kontenerami (nie raz o tym powiedział wspominał nasz dyrektor techniczny).

Jednakże nie zdziwi cię sytuacja, w której w kodzie strony używany jest gotowy framework, którego zastosowanie wiąże się z ograniczeniami dalszej eksploatacji. I jeśli w „zwykłym środowisku” można sobie z tym łatwo poradzić, to w Kubernetesa takie zachowanie może stać się problemem, zwłaszcza gdy spotykasz się z tym po raz pierwszy. Chociaż pomysłowy umysł może zaproponować infrastrukturalne rozwiązania, wydające się oczywiste i nawet niezłe na pierwszy rzut oka… ważne jest, aby pamiętać, że większość sytuacji można i należy rozwiązywać architektonicznie.

Przeanalizujemy popularne rozwiązania alternatywne do przechowywania plików, które mogą prowadzić do nieprzyjemnych konsekwencji podczas eksploatacji klastra, a także wskażemy na bardziej właściwą drogę.

Przechowywanie statyki

Aby zobrazować, rozważmy aplikację internetową, która używa pewnego generatora statyki do uzyskania zestawu obrazków, stylów i innych. Na przykład, w frameworku PHP Yii znajduje się wbudowany menedżer zasobów, który generuje unikalne nazwy katalogów. W związku z tym, na wyjściu mamy zestaw ścieżek, które z góry nie kolidują ze sobą (zostało to zrobione z kilku powodów — na przykład, aby uniknąć duplikatów przy użyciu tego samego zasobu przez wiele komponentów). Tak, z pudełka, przy pierwszym dostępie do modułu zasobu internetowego następuje generowanie i układanie statyki (w rzeczywistości — często linków symbolicznych, ale o tym później) z unikalnym dla tego wdrożenia wspólnym katalogiem głównym:

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

Jakie to może mieć konsekwencje w kontekście klastra?

Najprostszy przykład

Weźmy dość powszechny przypadek, gdy przed PHP stoi nginx do rozdzielania statyki i przetwarzania prostych żądań. Najprostszy sposób — Deployment z dwoma kontenerami:

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

W uproszczeniu konfiguracja nginx sprowadza się do następnych elementów:

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

Przy pierwszym dostępie do witryny w kontenerze PHP pojawiają się zasoby. Jednak w przypadku dwóch kontenerów w ramach jednego pod’a — nginx nie wie nic o tych plikach statycznych, które (zgodnie z konfiguracją) powinny być właściwie dostarczane. W rezultacie, na wszystkie zapytania o pliki CSS i JS klient zobaczy błąd 404. Najprostszym rozwiązaniem tutaj będzie zorganizowanie wspólnego katalogu dla kontenerów. Prymitywna wersja — wspólna 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

Teraz generowane w kontenerze pliki statyczne są poprawnie dostarczane przez nginx. Przypominam, że jest to prymitywne rozwiązanie, więc jest dalekie od ideału i ma swoje niuanse oraz niedociągnięcia, o których mowa poniżej.

Bardziej zaawansowane magazynowanie

Wyobraźmy sobie sytuację, w której użytkownik wszedł na stronę, załadował stronę z istniejącymi w kontenerze stylami, a podczas gdy ją czytał, ponownie wdrożyliśmy kontener. W katalogu zasobów zrobiło się pusto i potrzebne jest zapytanie do PHP, aby uruchomić generację nowych. Jednak nawet po tym linki do starej statyki będą nieaktualne, co doprowadzi do błędów wyświetlania statyki.

Poza tym, mamy prawdopodobnie bardziej lub mniej obciążony projekt, więc jedna kopia aplikacji nie będzie wystarczająca:

  • Skalujmy Deployment do dwóch replik.
  • Przy pierwszym dostępie do strony w jednej replikacji zostały utworzone zasoby.
  • W pewnym momencie ingress postanowił (w celu równoważenia obciążenia) wysłać zapytanie do drugiej repliki, a tam tych zasobów jeszcze nie ma. A może ich tam już nie ma, ponieważ używamy RollingUpdate i obecnie przeprowadzamy wdrożenie.

W rezultacie ponownie pojawiają się błędy.

Aby nie stracić starych zasobów, można zmienić emptyDir na hostPath, przechowując statykę fizycznie na węźle klastra. Podejście to jest złe, ponieważ w praktyce musimy przywiązać się do konkretnego węzła klastra ze swoją aplikacją, ponieważ - w przypadku przeniesienia na inne węzły - katalog nie będzie zawierał potrzebnych plików. W przeciwnym razie wymagana jest jakaś synchronizacja tła katalogu między węzłami.

Jakie są możliwe rozwiązania?

  1. Jeśli sprzęt i zasoby na to pozwalają, można skorzystać z cephfs do organizacji równo dostępnego katalogu pod potrzeby statyki. Oficjalna dokumentacja zaleca dyski SSD, co najmniej potrójną replikację oraz stabilne "grube" połączenie między węzłami klastra.
  2. Mniej wymagającą opcją będzie organizacja serwera NFS. Jednakże wtedy należy uwzględnić możliwe wydłużenie czasu odpowiedzi na przetwarzanie zapytań przez serwer WWW, a jego odporność na awarie pozostawia wiele do życzenia. Konsekwencje awarii są katastrofalne: utrata montażu skazuje klaster na zagładę pod naporem obciążenia LA, które wzrasta do nieba.

Oprócz tego, dla wszystkich opcji tworzenia trwałego magazynu wymagany będzie w tle czyszczenie przestarzałych zestawów plików, zgromadzonych przez pewien czas. Przed kontenerami z PHP można postawić DaemonSet z buforującymi nginx, które będą przechowywać kopie zasobów przez ograniczony czas. To zachowanie można łatwo skonfigurować za pomocą proxy_cache z głębokością przechowywania w dniach lub gigabajtach przestrzeni dyskowej.

Połączenie tej metody z wymienionymi powyżej rozproszonymi systemami plików daje ogromne możliwości, a jedynym ograniczeniem jest budżet i techniczny potencjał tych, którzy będą to wdrażać i wspierać. Z doświadczenia możemy jednak stwierdzić, że im prostszy system, tym stabilniej działa. Dodanie takich warstw znacznie komplikuje utrzymanie infrastruktury, co z kolei zwiększa czas poświęcany na diagnostykę i naprawę w przypadku awarii.

Rekomendacja

Jeśli wprowadzenie proponowanych wariantów przechowywania wydaje się zbyt skomplikowane (drogo…), warto przyjrzeć się problemowi z innej strony. A mianowicie — zagłębić się w architekturę projektu i wyeliminować problem w kodzie, przywiązując się do jakiejś statycznej struktury danych w obrazie, jednoznaczne określenie zawartości lub procedury "podgrzewania" i/lub prekompilacji zasobów na etapie budowy obrazu. W ten sposób uzyskujemy w pełni przewidywalne zachowanie i taki sam zestaw plików dla wszystkich środowisk i replik uruchomionej aplikacji.

Wracając do konkretnego przykładu z frameworkiem Yii i nie zagłębiając się w jego strukturę (co nie jest celem tego artykułu), wystarczy wskazać dwa popularne podejścia:

  1. Zmienić proces budowy obrazu tak, aby umieszczać zasoby w przewidywalnym miejscu. Tak proponują/wdrażają w rozszerzeniach takich jak yii2-static-assets.
  2. Określać konkretne hashe dla katalogów zasobów, jak opisano na przykład w tej prezentacji (zaczynając od slajdu nr 35). Tak przy okazji, autor prezentacji ostatecznie (i nie bez podstaw!) zaleca, aby po zbudowaniu zasobów na serwerze budowania, przesłać je do centralnego magazynu (takiego jak S3), przed którym należy postawić CDN.

Przesyłane pliki

Inny przypadek, który z pewnością się sprawdzi przy przenoszeniu aplikacji do klastra Kubernetes — przechowywanie plików użytkowników w systemie plików. Na przykład znów mamy aplikację na PHP, która przyjmuje pliki przez formularz przesyłania, coś z nimi robi w trakcie działania i zwraca z powrotem.

Miejsce, w którym te pliki powinny być umieszczane, w kontekście Kubernetes powinno być wspólne dla wszystkich replik aplikacji. W zależności od złożoności aplikacji i potrzeby organizacji trwałości tych plików, takim miejscem mogą być wspomniane powyżej opcje shared-urządzeń, ale, jak widzimy, mają one swoje wady.

Rekomendacja

Jednym z rozwiązań jest użycie magazynu zgodnego z S3 (nawet jakąś odmianę kategorii self-hosted, taką jak minio). Przejście na pracę z S3 wymaga zmian na poziomie kodu, a jak będzie odbywało się dostarczanie treści na froncie, już pisaliśmy.

Sesje użytkowników

Osobno należy również zwrócić uwagę na organizację przechowywania sesji użytkowników. Często są to również pliki na dysku, co w kontekście Kubernetes doprowadzi do ciągłych żądań autoryzacji od użytkownika, jeśli jego zapytanie trafi do innego kontenera.

Częściowo problem można rozwiązać włączając stickySessions na ingress (funkcja jest wspierana we wszystkich popularnych kontrolerach ingress — więcej informacji w naszym przeglądzie), aby powiązać użytkownika z konkretnym pod’em z aplikacją:

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

Ale to nie rozwiąże problemów przy kolejnych wdrożeniach.

Rekomendacja

Bardziej prawidłowym rozwiązaniem będzie przetłumaczenie aplikacji na przechowywanie sesji w memcached, Redis i podobnych rozwiązaniach — w ogóle, całkowite wyeliminowanie opcji plikowych.

Podsumowanie

Zważywszy na opisywane w tekście rozwiązania infrastrukturalne, zasługują one na zastosowanie jedynie w formie tymczasowych "kół ratunkowych" (co brzmi bardziej elegancko po angielsku jako workaround). Mogą być aktualne na wczesnych etapach migracji aplikacji do Kubernetes, ale nie powinny "zakorzeniać się".

Ogólny zalecany kierunek sprowadza się do tego, aby pozbyć się ich na rzecz architektonicznych usprawnień aplikacji zgodnych z już dobrze znanym wielu 12-Factor App. Jednak doprowadzenie aplikacji do stanu stateless nieuchronnie oznacza, że będą potrzebne zmiany w kodzie, i tu ważne jest znalezienie równowagi między możliwościami/wymaganiami biznesu a perspektywami realizacji i utrzymania wybranego kierunku.

P.S.

Przeczytaj także na naszym blogu:

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster