
Nella costruzione del processo CI/CD utilizzando Kubernetes, può sorgere un problema di incompatibilità tra i requisiti della nuova infrastruttura e l'applicazione a essa destinata. In particolare, durante la fase di costruzione dell'applicazione è importante ottenere una un'immagine che verrà utilizzata in tutti gli ambienti e i cluster del progetto. Questo principio si basa su una corretta gestione dei container (ne abbiamo parlato già il nostro CTO).
Tuttavia, si possono osservare situazioni in cui nel codice del sito viene utilizzato un framework preesistente, il cui utilizzo impone delle limitazioni sulla sua successiva operatività. E se in un 'ambiente normale' questo è facile da gestire, in Kubernetes un comportamento del genere può diventare problematico, soprattutto quando ci si imbatte in esso per la prima volta. Anche se una mente ingegnosa è in grado di proporre soluzioni infrastrutturali che sembrano ovvie e persino buone a prima vista... è importante ricordare che la maggior parte delle situazioni può e deve essere affrontata architettonicamente.
Analizziamo le popolari soluzioni alternative per la memorizzazione dei file, che possono portare a conseguenze indesiderate durante l'utilizzo del cluster, e indichiamo un percorso più corretto.
Memorizzazione della statica
Per illustrare, consideriamo un'applicazione web che utilizza un certo generatore di statiche per ottenere un insieme di immagini, stili e altro. Ad esempio, nel framework PHP Yii c'è un gestore di asset integrato che genera nomi di directory univoci. Di conseguenza, all'uscita si ottiene un insieme di percorsi completamente non sovrapposti tra loro per la statica del sito (questo è stato fatto per diversi motivi — ad esempio, per evitare duplicati utilizzando la stessa risorsa da più componenti). Così, di default, al primo accesso al modulo della risorsa web avviene la generazione e la distribuzione della statica (in realtà, spesso sono collegamenti simbolici, ma di questo parleremo più avanti) con una cartella radice comune unica per questa distribuzione:
-
webroot/assets/2072c2df/css/… -
webroot/assets/2072c2df/images/… -
webroot/assets/2072c2df/js/…
Qual è il rischio di questo nel contesto del cluster?
Un esempio semplice
Prendiamo un caso relativamente comune in cui davanti a PHP c'è nginx per la distribuzione della statica e la gestione delle richieste semplici. Il modo più semplice è Deployment con due container:
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 riassume nel seguente modo:
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;
}
} Alla prima richiesta al sito nel contenitore con PHP appaiono le risorse. 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 vedrà un errore 404. La soluzione più semplice sarebbe quella di organizzare una directory condivisa tra i contenitori. Una soluzione primitiva è una condivisa 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, pertanto è lontana dall'essere ideale e presenta le sue sfide e limiti, di cui parlerò in seguito.
Un archivio più avanzato
Immaginiamo ora una situazione in cui un utente accede al sito, carica la pagina con gli stili esistenti nel contenitore e mentre legge quella pagina, abbiamo ridistribuito nuovamente il contenitore. La directory degli asset è diventata vuota e è necessaria una richiesta a PHP per avviare la generazione di nuovi asset. Tuttavia, anche dopo questo, i collegamenti alla vecchia statica saranno obsoleti, il che porterà a 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:
- Scalando Deployment fino a due repliche.
- Alla prima richiesta al sito, in una replica sono stati creati asset.
- A un certo punto, l'ingress ha deciso (per motivi di bilanciamento del carico) di inviare la richiesta alla seconda replica, e lì quegli asset non ci sono ancora. Oppure potrebbe darsi che non ci siano già, perché stiamo utilizzando
RollingUpdatee al momento stiamo eseguendo il deployment.
In generale, il risultato sono di nuovo errori.
Per non perdere i vecchi asset, possiamo modificare emptyDir in hostPath, memorizzando fisicamente la statica sul nodo del cluster. Questo approccio è pessimo perché dobbiamo effettivamente collegarci a un nodo specifico del cluster con la nostra applicazione, poiché - in caso di spostamento su altri nodi - la directory non conterrà i file necessari. Oppure è necessaria una qualche sincronizzazione in background della directory tra i nodi.
Quali sono le possibili soluzioni?
- Se l'hardware e le risorse lo permettono, possiamo utilizzare per organizzare una directory accessibile a tutti per le esigenze della statica. raccomanda dischi SSD, almeno una replica tripla e una connessione "spessa" stabile tra i nodi del cluster.
- Un'opzione meno esigente sarebbe organizzare un server NFS. Tuttavia, in tal caso è necessario tenere conto di un possibile aumento del tempo di risposta nel trattamento delle richieste da parte del server web, e la tolleranza ai guasti ne risentirà. Le conseguenze di un guasto sono catastrofiche: la perdita di mount condanna il cluster alla morte sotto la pressione del carico LA, che sale alle stelle.
Inoltre, per tutte le opzioni di creazione di uno storage permanente sarà necessaria una pulizia in background dei set di file obsoleti accumulati in un certo intervallo di tempo. Prima dei contenitori PHP possiamo impostare DaemonSet di nginx in cache, che conserveranno copie degli asset per un tempo limitato. Questo comportamento è facilmente configurabile tramite proxy_cache con una profondità di archiviazione in giorni o gigabyte di spazio su disco.
L'unione di questo metodo con i sistemi di file distribuiti sopra menzionati offre un enorme campo di fantasie, con il limite solo nel budget e nel potenziale tecnico di coloro che lo realizzeranno e lo manterranno. Dalla nostra esperienza, possiamo dire che più semplice è il sistema, più stabile sarà il suo funzionamento. Aggiungendo strati simili, mantenere l'infrastruttura diventa molto più complesso, e con ciò aumenta anche il tempo speso nella diagnosi e nel ripristino in caso di guasti.
Raccomandazione
Se l'implementazione delle soluzioni di archiviazione proposte ti sembra ingiustificata (complessa, costosa...), vale la pena guardare la situazione da un altro punto di vista. In particolare, scavare nell'architettura del progetto e radicare il problema nel codice, legandosi a una certa struttura dei dati statica nell'immagine, definizione univoca del contenuto o della procedura di "riscaldamento" e/o precompilazione degli asset durante la fase di costruzione dell'immagine. In questo modo otteniamo un comportamento assolutamente prevedibile e un insieme identico di file per tutti gli ambienti e le repliche dell'applicazione avviata.
Se torniamo all'esempio specifico con il framework Yii e non ci addentriamo nel suo funzionamento (il che non è l'obiettivo dell'articolo), basta indicare due approcci popolari:
- Modificare il processo di costruzione dell'immagine in modo da posizionare gli asset in un luogo prevedibile. Questo è ciò che viene proposto/implementato in estensioni come .
- Definire hash specifici per le directory degli asset, come descritto, ad esempio, in (a partire dalla diapositiva n. 35). A proposito, l'autore della relazione alla fine (e non senza ragioni!) consiglia di caricare gli asset in un repository centrale (come S3) dopo la loro costruzione nel server di build, posizionando davanti ad esso un CDN.
File caricati
Un altro caso che sicuramente funzionerà quando si sposta l'applicazione in un cluster Kubernetes è l'archiviazione di file utente nel file system. Ad esempio, abbiamo di nuovo un'applicazione PHP, che riceve file tramite un modulo di caricamento, esegue qualcosa su di essi durante l'elaborazione e li restituisce.
Il luogo in cui 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 organizzare la persistenza di questi file, tale luogo potrebbe essere una delle opzioni per dispositivi shared menzionate sopra, ma, come vediamo, hanno i loro svantaggi.
Raccomandazione
Una delle opzioni per la soluzione è l'uso di uno storage compatibile con S3 (anche se si tratta di una varietà della categoria self-hosted come minio). Passare a lavorare con S3 richiederà cambiamenti a livello di codice, e come avverrà la consegna del contenuto sul frontend, lo abbiamo già .
Sessioni utente
Vale la pena notare separatamente l'organizzazione della memorizzazione delle sessioni utente. Spesso anche questi sono file su disco, il che, nell'ambito di Kubernetes, porterà a richieste di autorizzazione costanti da parte dell'utente, se la sua richiesta finirà in un altro contenitore.
Parzialmente, il problema si risolve attivando stickySessions sull'ingress (la funzionalità è supportata da tutti i controller ingress più popolari — approfondisci in ), per collegare l'utente a un pod specifico 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 deployment ripetuti.
Raccomandazione
Un modo più corretto sarebbe trasferire l'applicazione a memorizzare le sessioni in memcached, Redis e soluzioni simili — in sostanza, rinunciare completamente alle opzioni basate su file.
Conclusione
Le soluzioni infrastrutturali discusse nel testo meritano di essere utilizzate solo come "protesi" temporanee (che suona più elegantemente in inglese come workaround). Possono essere pertinenti nelle prime fasi di migrazione dell'applicazione in Kubernetes, ma non dovrebbero "prendere piede".
Il percorso raccomandato è quello di sbarazzarsi di esse a favore di un miglioramento architettonico dell'applicazione in conformità con il già noto . Tuttavia, rendere l'applicazione stateless significa inevitabilmente che saranno necessari cambiamenti nel codice, ed è importante trovare un equilibrio tra le opportunità/richieste del business e le prospettive di implementazione e manutenzione del percorso scelto.
P.S.
Leggi anche nel nostro blog:
- «»;
- «»;
- «» (da Red Hat);
- «».
Fonte: habr.com
