Insieme ad Aruba, scopriamo come garantire l’alta disponibilità della tua applicazione web con Kubernetes gestito.
1. Introduzione al Kubernetes gestito
Kubernetes è diventato lo standard ‘de facto’ per l’orchestrazione di container nel panorama dell’IT moderno. Questa piattaforma open-source, originariamente sviluppata da Google, permette di automatizzare il deployment, lo scaling e la gestione di applicazioni containerizzate. La sua importanza risiede nella capacità di semplificare la gestione di applicazioni complesse, migliorare l’efficienza operativa e garantire la scalabilità in ambienti cloud e on-premise.
Il Kubernetes gestito di Aruba offre numerosi vantaggi rispetto a una configurazione self-managed. Innanzitutto, elimina la complessità dell’installazione e della manutenzione dell’infrastruttura Kubernetes, consentendo ai team di sviluppo di concentrarsi sulla creazione e il miglioramento delle applicazioni. Inoltre, Aruba si occupa degli aggiornamenti di sicurezza e delle patch, garantendo che il cluster sia sempre aggiornato e protetto.
Un aspetto fondamentale del Aruba-managed Kubernetes è il concetto di alta disponibilità, che si applica sia al control plane che al data plane. L’alta disponibilità del control plane assicura che i componenti master di Kubernetes siano replicati e distribuiti su più nodi di availability in zone diverse, garantendo la continuità operativa anche in caso di guasti hardware o di rete. Questo è particolarmente importante per mantenere la stabilità e la gestibilità del cluster.
L’utilizzatore può beneficiare dell’alta affidabilità del dataplane Kubernetes grazie alla disponibilità di avere Kubernetes NodePool su Availability Zone diverse. In Aruba il concetto di Kubernetes NodePool è locale ad una availability Zone. Questo significa che se abbiamo nodepool in availability zone diverse allora l’applicazione, e quindi il dataplane Kubernetes, sarà resiliente a fallimenti locali ad una singola availability zone perchè grazie a Kubernetes l’applicazione può essere schedulata e replicata su nodi diversi.
Questa configurazione è cruciale per garantire la resilienza delle applicazioni degli utenti. Distribuendo i nodi del cluster su zone di disponibilità diverse, si crea un’architettura robusta che può resistere a interruzioni localizzate e garantire la continuità del servizio.
2. Concetti chiave dell’alta disponibilità
Le zone di disponibilità (AZ) sono infrastrutture fisicamente separate all’interno di una regione cloud, ciascuna con la propria alimentazione, raffreddamento e connettività di rete. Queste zone sono progettate per essere isolate le une dalle altre, in modo che un problema in una zona non influisca sulle altre. L’importanza delle zone di disponibilità risiede nella loro capacità di fornire ridondanza e resilienza a livello di infrastruttura, consentendo alle applicazioni di rimanere operative anche in caso di guasti localizzati o disastri.
Le strategie di resilienza per le applicazioni web si basano su diversi principi chiave:
- Ridondanza: Distribuzione di copie multiple dell’applicazione su diverse zone di disponibilità.
- Bilanciamento del carico: Distribuzione uniforme del traffico tra le istanze dell’applicazione.
- Failover automatico: Capacità di reindirizzare il traffico verso istanze funzionanti in caso di guasti.
- Stato distribuito: Memorizzazione dei dati di stato in modo distribuito per evitare singoli punti di fallimento.
- Monitoraggio e auto-guarigione: Implementazione di sistemi che possono rilevare e risolvere automaticamente i problemi.
3. Configurazione del Cluster Kubernetes Multi-zona
L’alta disponibilità del control plane viene garantita selezionando l’opzione HA sul pannello di gestione Aruba (o direttamente tramite API).. In questa configurazione, il control plane di Kubernetes è distribuito su tre zone di disponibilità per garantire la continuità operativa e l’alta disponibilità del sistema di gestione, in quanto lo stato del cluster risiede su etcd, che richiede tre zone per mantenere la resilienza in caso di caduta di una zona.
L’architettura di dataplane è, al contrario, decisa dal cliente e si basa sulla distribuzione strategica dei nodi del cluster su infrastrutture fisicamente separate, in base alle necessità delle proprie applicazioni. I node pool sono un concetto logico che sente di raggruppare uno o più nodi computazionali ( a seconda della cardinalità impostata) che sono simili per taglia e topologia
Ecco i passaggi principali per distribuire i nodi su zone diverse utilizzando Aruba Managed Kubernetes:
- Creazione del cluster: Utilizzare la console di Aruba Cloud per creare un nuovo cluster Kubernetes, selezionando l’opzione per il multi-zona.
- Configurazione dei node pool: Definire almeno due node pool, uno per ciascuna zona di disponibilità. Ad esempio:
nodePools:
- name: "pool1"
nodes: 3
instance: "m1.medium"
dataCenter: "zone-1"
- name: "pool2"
nodes: 3
instance: "m1.medium"
dataCenter: "zone-2"
Code language: JavaScript (javascript)
- Schema Architetturale:
Zone: 2 (es. zone-1 e zone-2)
Node Pools: 2 (uno per zona)
Nodi per Node Pool: 3 nodi per zona, per un totale di 6 nodi
Questo garantisce ridondanza tra le zone e bilanciamento del carico.
L’etichettatura dei nodi, con Aruba managed Kubernetes, viene eseguita in maniera automatica:
DC-A > az1
DC-B > az2
DC-C > az3
- Configurazione delle informazioni di topologia: Applicata su risorse come Pod o Deployment.
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
name: web-app
Code language: YAML (yaml)
Chiavi principali:
- maxSkew: Definisce la differenza massima consentita nel numero di pod tra le zone. Un valore di 1 significa che la differenza tra la zona con più pod e quella con meno pod non può superare 1 unità.
- topologyKey: L’etichetta topology.kubernetes.io/zone viene utilizzata per identificare le diverse zone di disponibilità in cui distribuire i pod.
- whenUnsatisfiable: L’opzione DoNotSchedule indica che se non è possibile rispettare il vincolo di distribuzione, il pod non verrà schedulato.
- labelSelector: Specifica quali pod devono seguire questa politica di distribuzione, attraverso le loro etichette (in questo caso, name: web-app).
4. Design e best practices
Fare le giuste scelte di design è fondamentale per poter ottenere una soluzione che sia altamente affidabile e facilmente manutenibile. Di seguito i punti chiave:
- Distribuzione Multi-Zona: La configurazione prevede la distribuzione dei pod su diverse zone, migliorando la resilienza dell’applicazione rispetto a guasti locali e zonali.
- Bilanciamento del carico tramite Ingress: L’uso di un solo Ingress per esporre l’applicazione centralizza il traffico esterno in un unico punto di ingresso, semplificando l’architettura e riducendo la complessità operativa. In base al carico di traffico atteso potrebbe essere opportuno riservare dei nodi per la gestione del solo traffico in ingresso, etichettando opportunamente (e.g. con la label “ingress”). Si consideri questo esempio, non esuasitvo, di specifica:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nginx-ingress
namespace: nginx-ingress
spec:
selector:
matchLabels:
app: nginx-ingress
template:
metadata:
labels:
app: nginx-ingress
app.kubernetes.io/name: nginx-ingress
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- az2
tolerations:
- key: "ingress"
operator: "Exists"
effect: "NoSchedule"
Code language: YAML (yaml)
In più, utilizzando un ingress, non è più necessario istanziare un LoadBalancer separato per ogni servizio, poiché l’Ingress si occupa già di gestire il traffico HTTP/HTTPS e instradarlo verso il backen corretto, con conseguente risparmio di costo e tempi di gestione.
- Deployment: Utilizziamo un Deployment per gestire il ciclo di vita dei pod in modo flessibile e automatizzato, aggiungendo le specifiche di topologySpreadConstraints
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp-deployment
spec:
replicas: 3
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
name: webapp
containers:
- name: webapp
image: webapp:1.0
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Code language: YAML (yaml)
Si faccia attenzione all’impostazione di request e limit per la nostra applicazione. Su un sistema carico alcuni container potrebbero essere rimossi per recuperare risorse come CPU e RAM. Idealmente, vorremmo eliminare i container meno importanti. Per questo, in k8s, è possibile definire tre classi di servizio (QoS classes) per i container in base alle impostazioni di Request e Limit: Guaranteed, Burstable e Best-Effort, in ordine decrescente di priorità. In particolare:
- Se i limiti e, facoltativamente, le richieste (diversi da 0) sono impostati tra le Resources dei container e sono uguali, il pod viene classificato come Guaranteed.
- Se le richieste e facoltativamente i limiti sono impostati (diversi da 0) tra le Resources dei container e non sono uguali, il pod viene classificato come Burstable. Quando i limiti non sono specificati, vengono impostati di default alla capacità del nodo
- Se le richieste e i limiti non sono impostati il pod viene classificato come Best-Effort.
- PodDisruptionBudget (PDB): Definiamo un PDB che assicura la disponibilità di almeno due pod durante operazioni di manutenzione, garantendo continuità del servizio.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: webapp-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: webapp
Code language: YAML (yaml)
Il PDB garantisce che almeno 2 pod rimangano sempre disponibili durante operazioni di manutenzione pianificate.
- Ingress e ClusterIP Service: Utilizziamo una Ingress rules per gestire il traffico esterno, e un Service di tipo ClusterIP per inoltrare il traffico ai pod senza l’uso di un LoadBalancer aggiuntivo, ottimizzando la gestione del traffico.
apiVersion: v1
kind: Service
metadata:
name: webapp-service
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 8080
selector:
app: webapp
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: webapp-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /webapp
pathType: Prefix
backend:
service:
name: webapp-service
port:
number: 80
Code language: YAML (yaml)
L’Ingress può essere configurato per gestire il routing HTTP/HTTPS con path-based routing e SSL termination.
5. Monitoraggio e gestione della continuità operativa
Per garantire l’alta disponibilità della web app, è fondamentale implementare un sistema di monitoraggio robusto. È possibile utilizzare diversi strumenti di monitoraggio, tra cui:
- Prometheus: Per la raccolta di metriche del cluster e delle applicazioni.
- Grafana: Per la visualizzazione dei dati e la creazione di dashboard personalizzate.
- Alertmanager: Per la gestione e l’invio di notifiche in caso di problemi.
Simulazione del fault di un nodo
Per dimostrare la resilienza dell’architettura, possiamo simulare il drain dei nodi in una zona specifica per verificare la rischedulazione automatica dei pod.
Passaggi:
Identificare i nodi nella zona target:
bash
kubectl get nodes -l topology.kubernetes.io/zone=az1
Code language: Bash (bash)
Eseguire il drain dei nodi:
bash
kubectl drain <node-name> \
--ignore-daemonsets \
--delete-emptydir-data \
--timeout=300s
Code language: Bash (bash)
Monitorare la rischedulazione:
bash
kubectl get pods -o wide -w
Code language: Bash (bash)
Risultato atteso:
- I pod verranno rischedulati solo sui nodi rimanenti della zona, garantendo che il traffico continui ad essere bilanciato sulle due zone. Iterando il drain su tutti i nodi di una zona i pod verranno schedulati sulla zona rimanente fintanto che non si viola la maxSkew.
- Il PDB garantirà almeno 2 pod attivi durante il processo
- L’Ingress continuerà a distribuire il traffico ai pod attivi
In conclusione, questa simulazione verifica l’efficacia delle configurazioni di alta disponibilità implementate e la capacità dell’infrastruttura di mantenere il servizio attivo anche in caso di perdita di una zona.