Photo by Ralph (Ravi) Kayden on Unsplash
EKS Autoscaling: Karpenter (versione italiana)
Come configurare il node autoscaling per EKS con Terraform: parte seconda
If you're looking for the English version of this article, click here.
Karpenter
Karpenter è una nuova soluzione di node lifecycle management sviluppata da AWS Labs e rilasciata in GA al re:Invent 2021.
Si tratta di un prodotto open-source giovane ma promettente, nato come risposta ad alcune delle complessità di Cluster Autoscaler.
Cluster Autoscaler, infatti, con tutti i tag che devono corrispondere tra i vari tipi di risorse cloud, e permessi e ruoli da associare, non è banale da configurare (tanto che gli amici di Google Cloud hanno pensato bene di fornirlo già configurato su GKE...). Una volta reso funzionante è affidabile, ma la sua complessità di configurazione iniziale (paragonata ad altre soluzioni) spesso può risultare scoraggiante.
Rispetto a Cluster Autoscaler (potete leggere il mio articolo dedicato alla sua configurazione), Karpenter ha un approccio diverso all'autoscaling:
dietro le quinte, non si affida più all'ASG, ma invoca la creazione dei nodi direttamente dal cloud provider (nel caso di AWS, tramite un Launch Template);
ci sono molte meno configurazioni da fare rispetto a Cluster Autoscaler (almeno su AWS)
nel momento in cui rileva la necessità di allocare risorse computazionali nel cluster, è in grado di calcolare quale sia l'instance type più adeguato e lanciare un unico nuovo nodo che possa, da solo, soddisfare tutto il fabbisogno richiesto; non è necessario decidere a priori con quale/i instance type configurare il node group
Vediamo come funziona!
Come configurare Karpenter su EKS
Ok, ho detto che bisogna configurare meno tag di quanti se ne configurano su Cluster Autoscaler: ma qualcuno bisogna ancora assegnarlo :-)
Potete trovare una configurazione completa a questo link. In questo articolo vediamo assieme solo alcuni dettagli del codice.
Come prerequisito, è necessario aggiungere un ulteriore tag alle subnet della VPC in cui i nodi EKS vengono creati. Perciò, includendo i tag che normalmente si assegnano per EKS, la configurazione sarà:
public_subnet_tags = {
"kubernetes.io/cluster/${local.name}" = "shared"
"kubernetes.io/role/elb" = 1
}
private_subnet_tags = {
"kubernetes.io/cluster/${local.name}" = "shared"
"kubernetes.io/role/internal-elb" = 1
# Tags subnets for Karpenter auto-discovery
"karpenter.sh/discovery" = local.name
}
La definizione dei node group è più semplice, perché la sola configurazione necessaria è quella che serve a creare il primo nodo del cluster. Tutto il resto verrà gestito da Karpenter:
eks_managed_node_groups = {
karpenter = {
instance_types = ["t3.medium"]
create_security_group = false
attach_cluster_primary_security_group = true
enable_monitoring = true
min_size = 1
max_size = 1
desired_size = 1
iam_role_additional_policies = [
# Required by Karpenter
"arn:${local.partition}:iam::aws:policy/AmazonSSMManagedInstanceCore"
]
}
}
L'installazione di Karpenter su questo primo nodo possiamo sempre farla con Helm tramite Terraform:
resource "helm_release" "karpenter" {
namespace = "karpenter"
create_namespace = true
name = "karpenter"
repository = "https://charts.karpenter.sh"
chart = "karpenter"
version = "v0.13.2"
...
}
A questo punto, dopo che Helm ha installato la CRD del provisioner Karpenter, possiamo inserirne la configurazione:
I valori indicati nel codice sono puramente illustrativi. Verificate sempre quali valori sono più adeguati per ogni specifico caso d'uso.
resource "kubectl_manifest" "karpenter_provisioner" {
yaml_body = <<-YAML
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
name: nodegroup2
spec:
requirements:
# Include general purpose instance families
- key: karpenter.k8s.aws/instance-family
operator: In
values: [c5, m5, r5]
# Exclude some instance sizes
- key: karpenter.k8s.aws/instance-size
operator: NotIn
values: [nano, micro, small, 24xlarge, 18xlarge, 16xlarge, 12xlarge]
# Exclude a specific instance type
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
taints:
- key: dedicated
value: ${local.nodegroup2_label}
effect: "NoSchedule"
limits:
resources:
cpu: 16000
provider:
subnetSelector:
karpenter.sh/discovery: ${local.name}
securityGroupSelector:
karpenter.sh/discovery: ${local.name}
tags:
karpenter.sh/discovery: ${local.name}
role: group2
ttlSecondsAfterEmpty: 30
YAML
}
Questo esempio di configurazione del provisioner Karpenter include alcune scelte:
ho indicato un subset di
instance_family
ho escluso un subset di
instance_size
(quelle troppo piccole per evitare problemi di performance, quelle troppo grandi per evitare problemi di billing)ho limitato a 16 CPU le risorse che il provisioner può gestire; oltre questo valore non prende in carico nuove richieste di autoscaling
Altre possibilità di personalizzazione della configurazione sono disponibili nella documentazione ufficiale.
Scale-up su EKS con Karpenter
Ripetendo lo stesso test effettuato con Cluster Autoscaler, con Karpenter è quasi impercettibile il lasso di tempo tra l'istante in cui si richiedono nuove risorse computazionali e quello in cui il nodo viene creato, diversamente da Cluster Autoscaler che ha alcuni secondi di latenza in questa operazione.
Inoltre, avendo richiesto 5 nuove repliche tutte assieme, Karpenter ha calcolato quale fosse la scelta migliore come tipo di istanza in quel momento e per quella region:
Cluster Autoscaler, invece, nella stessa situazione ha creato nodi multipli fino a soddisfare la richiesta di risorse computazionali.
Scale-down su EKS con Karpenter
Lo scale-down di Karpenter è altrettanto rapido: quando un nodo risulta vuoto, entro il tempo indicato da ttlSecondsAfterEmpty (valore da settare sempre, altrimenti non c'è scale-down) il nodo viene terminato.
ATTENZIONE: le funzionalità descritte di seguito sono in fase di sviluppo. La descrizione qui inclusa è relativa al momento in cui l'articolo è scritto (19 luglio 2022).
Quello che Karpenter NON gestisce, però, è il consolidamento dei workload nel momento in cui i nodi sono sotto-utilizzati in termini di risorse.
Il caso d'uso di cui ho parlato nell'articolo su Cluster Autoscaler è particolare: poiché i workload sono job batch, una volta che l'esecuzione di tutti i job che insistono su un nodo è finita, non vi è la necessità di migrare alcun pod affinché il nodo risulti inutilizzato, e quindi viene terminato senza problemi.
Tuttavia, nel caso di workload non batch, come per esempio il deployment utilizzato per il test, nel momento in cui c'è una riduzione del numero di repliche del pod, Karpenter non consolida i pod su un numero di nodi sufficiente ad ospitarle; il cluster quindi distribuisce i pod attivi tra i nodi presenti, e, di conseguenza, nessun nodo risulta vuoto e può essere terminato.
Quindi, se ad un certo punto ho avuto per qualche motivo la necessità di avere un numero di repliche elevato (ad esempio per un picco di traffico), Karpenter avrà creato un nodo con risorse di calcolo importanti, che però non verrà mai terminato perché non c'è un meccanismo di consolidamento.
Le limitazioni alle tipologie di macchine inserite nella configurazione del provisioner servono quindi a mitigare il rischio che vengano creati nodi molto potenti e molto costosi che rischiano di non essere mai terminati.
Specifiche configurazioni di
pod affinity
,topology spread constraints
,pod disruption budget
possono essere aggiunte in alcuni casi per mitigare risultati inattesi.
Consolidamento dei workload: anteprima
Fortunatamente la funzionalità di consolidamento dei workload è in roadmap: è tracciata in una issue su Github che ha già una pull request associata per gettarne le basi.
L'abbiamo provato la patch in anteprima: dopo l'installazione è disponibile una nuova proprietà nella configurazione:
apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
name: nodegroup2
spec:
consolidation:
enabled: true
...
Al momento l'anteprima di questa funzionalità non prevede parametri di configurazione. Il risultato è che la funzione di consolidamento può non essere sempre efficace:
quando Karpenter fa eseguire tutti i pod sul minor numero di nodi possibile, sfruttando tutte le risorse disponibili nel cluster, in certi casi non lascia spazio nei nodi, quindi eventuali esecuzioni di cronjobs fanno sì che il cluster scali ogni pochi minuti
nel caso in cui due nodi abbiano un numero di pod simile, una parte dei pod continuano a essere spostati da un nodo all'altro per bilanciare il workload, perché non è presente alcun meccanismo di controllo che si limiti a candidare un solo nodo per volta alla rimozione. Si torna così nella situazione in cui nessun nodo si svuota né può essere terminato
Valutazioni finali
Cluster Autoscaler | Karpenter | |
complessità di configurazione | medio/alta (su AWS) | bassa |
velocità | medio/alta (configurabile) | elevata |
maturità delle funzionalità | alta | medio/bassa |
Karpenter si è dimostrato molto promettente. La sua velocità di reazione è impressionante e con ogni probabilità riuscirà a ritargliarsi il suo spazio nel panorama dei tool integrati con Kubernetes, grazie al team di AWS che ne cura lo sviluppo e che ha tutto l'interesse di arricchirlo. Può essere un'alternativa interessante per cluster non critici o per Cluster Autoscaleri d'uso specifici.
Per cluster in ambiente di produzione, Karpenter dà la sensazione di non essere ancora abbastanza maturo, ma è in rapido sviluppo ed è certamente da tenere sotto osservazione per valutarne le future evoluzioni.