
Nella costruzione del processo CI/CD utilizzando Kubernetes, a volte si presenta un problema di incompatibilità tra i requisiti della nuova infrastruttura e l'applicazione che deve essere portata in essa. In particolare, durante la fase di build dell'applicazione è importante ottenere uno snapshot che verrà utilizzato nei tutti ambienti e nei cluster del progetto. Questo principio è alla base della corretta e il nostro CTO l'ha ribadito in più occasioni. Tuttavia, non è raro imbattersi in situazioni in cui nel codice del sito viene utilizzato un framework preconfezionato che impone restrizioni sul suo successivo utilizzo. Se in un “ambiente convenzionale” questo è facilmente gestibile, in Kubernetes tale comportamento può diventare un problema, soprattutto se ci si imbatte in esso per la prima volta. Anche se una mente ingegnosa è in grado di proporre soluzioni infrastrutturali che sembrano evidenti e persino buone a prima vista… è importante ricordare che la maggior parte delle situazioni può e deve
Tuttavia, non ci si può aspettare di vedere situazioni in cui nel codice del sito venga utilizzato un framework predefinito, la cui applicazione impone limiti sul suo ulteriore utilizzo. E se in un "ambiente normale" questo è facilmente gestibile, in Kubernetes un comportamento simile può diventare un problema, specialmente quando ci si imbatte in esso per la prima volta. Sebbene una mente ingegnosa possa offrire soluzioni infrastrutturali che sembrano evidenti e persino buone a prima vista... è importante ricordare che la maggior parte delle situazioni può e deve essere affrontata a livello architetturale..
Esamineremo le soluzioni workaround più popolari per l'archiviazione dei file, che possono portare a conseguenze indesiderate durante l'uso di un cluster, e indicheremo un percorso più corretto.
Archiviazione della statica
Per illustrare, consideriamo un'app web che utilizza un generatore di statica per ottenere un insieme di immagini, stili e altro. Ad esempio, nel framework PHP Yii è presente un gestore di asset integrato che genera nomi univoci per le directory. Di conseguenza, si ottiene un insieme di percorsi per la statica del sito che non si sovrappongono mai (questo è stato fatto per diverse ragioni — ad esempio, per escludere duplicati nel caso in cui molteplici componenti utilizzino la stessa risorsa). Così, di default, alla prima richiesta del modulo della risorsa web avviene la creazione e il posizionamento della statica (in realtà, spesso dei symlink, ma ne parleremo più avanti) con una directory radice comune unica per questo deployment:
-
webroot/assets/2072c2df/css/… -
webroot/assets/2072c2df/images/… -
webroot/assets/2072c2df/js/…
Quali sono le implicazioni nel contesto di un cluster?
Un esempio semplice
Consideriamo un caso piuttosto comune in cui Nginx gestisce la distribuzione dei file statici e l'elaborazione delle richieste semplici davanti a PHP. Il modo più semplice è Deployment con due contenitori:
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.confIn forma semplificata, la configurazione di Nginx si riduce al seguente:
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;
}
} Quando si accede per la prima volta al sito in un contenitore con PHP, vengono generati asset. Ma nel caso di due contenitori all'interno di uno stesso pod, nginx non sa nulla di questi file statici che (secondo la configurazione) dovrebbero essere gestiti proprio da lui. Di conseguenza, per tutte le richieste ai file CSS e JS, il cliente visualizzerà un errore 404. La soluzione più semplice è organizzare una directory condivisa tra i contenitori. Una versione primitiva è comune. 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.confOra i file statici generati nel contenitore vengono serviti correttamente da nginx. Ma ricordo che questa è una soluzione primitiva e, quindi, è lontana dall'essere ideale e ha le sue sfide e lacune che discuterò di seguito.
Storage più avanzato
Immaginiamo adesso una situazione in cui l'utente accede al sito, carica la pagina con gli stili presenti nel contenitore e, mentre legge questa pagina, ridistribuiamo il contenitore. La cartella degli asset diventa vuota e necessita di una richiesta a PHP per avviare la generazione di nuovi. Tuttavia, anche dopo ciò, i collegamenti alla vecchia statica saranno obsoleti, causando errori nella visualizzazione della statica.
Inoltre, molto probabilmente abbiamo un progetto piuttosto carico, il che significa che una sola copia dell'applicazione non sarà sufficiente:
- Scalaremo Deployment fino a due repliche.
- Al primo accesso al sito, in una replica vengono creati gli asset.
- A un certo punto, l'ingress ha deciso (per bilanciare il carico) di inviare la richiesta alla seconda replica, e lì quegli asset non ci sono ancora. O forse non ci sono già più, perché stiamo usando
RollingUpdatee al momento stiamo effettuando il deploy.
Insomma, il risultato è di nuovo errori.
Per non perdere i vecchi asset, possiamo modificare emptyDir con hostPath, archiviando fisicamente la statica sul nodo del cluster. Questo approccio è problematico perché dobbiamo di fatto collegarci a un nodo specifico del cluster. alla tua applicazione, perché — in caso di migrazione su altri nodi — la directory non conterrà i file necessari. È necessaria una sincronizzazione in background della directory tra i nodi.
Quali sono le possibili soluzioni?
- Se l'hardware e le risorse lo permettono, si può utilizzare per organizzare una directory condivisa per le esigenze di statico. raccomanda dischi SSD, un minimo di tre repliche e una connessione ‘spessa’ stabile tra i nodi del cluster.
- Una variante meno esigente sarà l'organizzazione di un server NFS. Tuttavia, in questo caso, bisogna considerare un possibile aumento dei tempi di risposta nella gestione delle richieste da parte del server web, e la resilienza ai guasti lascerà a desiderare. Le conseguenze di un guasto possono essere catastrofiche: la perdita del mount condanna il cluster a un crollo sotto la pressione della carico LA, che tende a salire alle stelle.
Oltre a tutto, per tutte le opzioni di creazione di archiviazione persistente sarà necessaria una pulizia in background dei set di file obsoleti, accumulati in un certo periodo di tempo. Prima dei contenitori PHP, puoi configurare DaemonSet da cache nginx che conserveranno copie degli asset per un periodo di tempo limitato. Questo comportamento è facilmente configurabile tramite proxy_cache con una profondità di archiviazione in giorni o gigabyte di spazio su disco.
Combinare questo metodo con i sistemi di file distribuiti menzionati sopra offre enormi possibilità, con l'unico limite rappresentato dal budget e dal potenziale tecnico di coloro che lo implementeranno e supporteranno. Dalla nostra esperienza, possiamo dire che più è semplice il sistema, più stabile è il suo funzionamento. Aggiungere strati come questi rende la manutenzione dell'infrastruttura molto più complicata, e di conseguenza aumenta il tempo necessario per la diagnosi e il ripristino in caso di guasti.
Raccomandazione
Se l'implementazione delle soluzioni di storage proposte vi sembra ingiustificata (complessa, costosa...), vale la pena guardare la situazione da un'altra angolazione. Avere in mente l'architettura del progetto e eliminare il problema nel codice, associandosi a una certa struttura dati statica nell'immagine, la definizione univoca del contenuto o della procedura di «riscaldamento» e/o precompilazione delle risorse nella fase di creazione dell'immagine. In questo modo otteniamo un comportamento completamente prevedibile e un set di file identico per tutti gli ambienti e le repliche dell'applicazione in esecuzione.
Tornando a un esempio specifico con il framework Yii, e senza approfondire la sua architettura (che non è l'obiettivo dell'articolo), è sufficiente menzionare due approcci popolari:
- Modificare il processo di creazione dell'immagine in modo da posizionare le risorse in un luogo prevedibile. Questo è suggerito/implementato in estensioni come .
- Definire hash specifici per le cartelle delle risorse, come spiegato, ad esempio, in (a partire dalla diapositiva n. 35). A proposito, l'autore della presentazione consiglia infine (e non senza ragione!) di caricare le risorse in uno storage centrale (come S3) dopo la creazione delle risorse sul server di build, davanti al quale posizionare un CDN.
File caricati
Un altro caso che sicuramente funzionerà quando si trasferisce un'applicazione in un cluster Kubernetes è lo stoccaggio dei file degli utenti nel filesystem. Ad esempio, abbiamo di nuovo un'applicazione PHP che riceve file tramite un modulo di upload, elabora qualcosa e restituisce i file.
Il luogo dove questi file devono essere collocati, nella realtà di Kubernetes, deve essere condiviso tra tutte le repliche dell'applicazione. A seconda della complessità dell'applicazione e della necessità di garantire la persistenza di questi file, quel luogo potrebbe essere le soluzioni di stoccaggio condiviso menzionate in precedenza, ma, come possiamo vedere, hanno i loro svantaggi.
Raccomandazione
Una delle soluzioni è l'uso di uno storage compatibile con S3 (anche una qualche variante di self-hosted come minio). Passare a lavorare con S3 richiederà modifiche a livello di codice, e come verrà restituito il contenuto sul frontend è qualcosa che .
Sessioni utente
È importante notare l'organizzazione della memorizzazione delle sessioni utente. Spesso si tratta di file su disco, il che, in un contesto Kubernetes, porterà a continue richieste di autorizzazione da parte dell'utente se la sua richiesta finisce in un altro contenitore.
In parte, il problema viene risolto attivando stickySessions sull'ingress (questa funzionalità è supportata in tutti i controller ingress più diffusi — maggiori dettagli possono essere trovati in ), per collegare l'utente a uno specifico pod con l'applicazione:
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: /Ma questo non eliminerà i problemi durante i deploy ripetuti.
Raccomandazione
Un modo più corretto sarebbe quello di migrare l'applicazione a memorizzazione delle sessioni in memcached, Redis e soluzioni simili — in sostanza, abbandonare completamente le varianti basate su file.
Conclusione
Le soluzioni infrastrutturali considerate nel testo sono adatte esclusivamente come "transitori" (un termine più elegante in inglese è workaround). Possono essere utili nelle prime fasi della migrazione di un'applicazione in Kubernetes, ma non dovrebbero "affondare radici".
Il percorso raccomandato si riduce a eliminare queste soluzioni a favore di una ristrutturazione architettonica dell'applicazione in conformità con il già ben noto. . Tuttavia, rendere l'applicazione stateless implica inevitabilmente modifiche al codice, e qui è importante trovare un equilibrio tra le esigenze/richieste aziendali e le prospettive di attuazione e manutenzione della strada scelta.
P.S.
Leggete anche nel nostro blog:
- «»;
- «»;
- «» (da Red Hat);
- «».
Fonte: habr.com
