Andiamo? Bash! Presentiamo shell-operator (una panoramica e un video della presentazione di KubeCon EU’2020).

Quest'anno la principale conferenza europea su Kubernetes — KubeCon + CloudNativeCon Europe 2020 — si è svolta in formato virtuale. Tuttavia, questo cambiamento di formato non ci ha impedito di presentare la nostra lunga prevista relazione "Go? Bash! Meet the Shell-operator", dedicata al nostro progetto Open Source. shell-operator.

In questo articolo, scritto sulla base della presentazione, viene presentato un approccio per semplificare il processo di creazione di operatori per Kubernetes e mostrato come, con il minimo sforzo, sia possibile realizzare il proprio utilizzando shell-operator.

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

Presentiamo il video della presentazione (~23 minuti in inglese, decisamente più informativo dell'articolo) e una sintesi principale in forma testuale. Andiamo!

In "Flant" ottimizziamo e automatizziamo costantemente. Oggi parleremo di un'altra affascinante concezione. Ecco a voi: cloud-native shell-scripting!

Cominciamo con il contesto in cui tutto ciò avviene, cioè Kubernetes.

API Kubernetes e controllori

L'API in Kubernetes può essere rappresentata come un certo server di file con directory dedicate a ogni tipo di oggetto. Gli oggetti (risorse) su questo server sono rappresentati da file YAML. Inoltre, il server ha un'API di base che consente di fare tre cose:

  • aggiornamenti fino ad aprile 2021. Il ramo LTS 2.22 sarà supportato fino ad aprile 2022, mentre il ramo 1.11 fino ad aprile 2020. Il supporto per il ramo 2.1 è stato interrotto. recuperare una risorsa in base al suo kind e nome;
  • modificare la risorsa (in questo caso il server memorizza solo gli oggetti "corretti" – tutti quelli malformati o destinati ad altre directory vengono scartati);
  • di seguire con la risorsa (in questo caso l'utente ottiene immediatamente la sua versione attuale/aggiornata).

In questo modo, Kubernetes funge da un certo tipo di server di file (per i manifest YAML) con tre metodi di base (sì, in realtà ce ne sono altri, ma per ora li tralasceremo).

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

Il problema è che il server è in grado solo di memorizzare informazioni. Per farla funzionare, è necessario controller – il secondo concetto più importante e fondamentale nel mondo di Kubernetes.

Si distinguono due tipi fondamentali di controller. Il primo estrae informazioni da Kubernetes, le elabora in base alla logica internamente definita e le restituisce a K8s. Il secondo estrae informazioni da Kubernetes, ma, a differenza del primo tipo, cambia lo stato di alcune risorse esterne.

Esaminiamo più nel dettaglio il processo di creazione di un Deployment in Kubernetes:

  • Il Deployment Controller (che fa parte di kube-controller-manager) riceve informazioni sul Deployment e crea il ReplicaSet.
  • Il ReplicaSet, sulla base di queste informazioni, crea due repliche (due pod), ma questi pod non sono ancora programmati.
  • Il pianificatore organizza i pod e aggiunge informazioni sui nodi nei loro YAML.
  • I kubelet apportano modifiche alle risorse esterne (ad esempio, Docker).

Poi, l'intera sequenza viene ripetuta in ordine inverso: il kubelet controlla i contenitori, calcola lo stato del pod e lo invia indietro. Il controller ReplicaSet riceve lo stato e aggiorna la condizione del set di repliche. Lo stesso avviene con il Deployment Controller, e infine l'utente riceve lo stato aggiornato (attuale).

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

Shell-operator

Quindi, alla base di Kubernetes c'è la collaborazione tra diversi controller (anche gli operatori Kubernetes sono controller). La domanda nasce spontanea: come creare il proprio operatore con il minimo sforzo? E qui entra in gioco il nostro shell-operator. Permette agli amministratori di sistema di creare i propri operatori utilizzando metodi familiari.

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

Un semplice esempio: la copia dei segreti

Esaminiamo un semplice esempio.

Supponiamo di avere un cluster Kubernetes. In esso c'è uno spazio dei nomi default con un certo Secret mysecret. Inoltre, nel cluster ci sono altri spazi dei nomi. A alcuni di essi è associata una certa etichetta. Il nostro obiettivo è copiare il Secret negli spazi dei nomi con l'etichetta.

Il compito si complica poiché nel cluster possono apparire nuovi spazi dei nomi, e alcuni di essi possono avere questa etichetta. D'altra parte, quando l'etichetta viene rimossa, il Secret deve essere rimosso anch'esso. Inoltre, il Secret stesso può subire variazioni: in questo caso, il nuovo Secret deve essere copiato in tutti gli spazi dei nomi con le etichette. Se il Secret viene accidentalmente eliminato in uno spazio dei nomi, il nostro operatore deve ripristinarlo immediatamente.

Ora che il compito è formulato, è tempo di passare alla sua implementazione tramite shell-operator. Ma prima vale la pena dire alcune parole sullo shell-operator.

Principi di funzionamento dello shell-operator

Come altre workload in Kubernetes, lo shell-operator funziona nel proprio pod. In questo pod, nella directory /hooks sono memorizzati i file eseguibili. Questi possono essere script in Bash, Python, Ruby, ecc. Tali file eseguibili li chiamiamo hook (hooks).

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

Lo shell-operator si iscrive agli eventi di Kubernetes e attiva questi hook in risposta a quegli eventi che ci interessano.

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

Come fa shell-operator a sapere quale hook eseguire e quando? Il fatto è che ogni hook ha due fasi. All'avvio, shell-operator esegue tutti gli hook con l'argomento --config — questa è la fase di configurazione. Solo dopo di essa, gli hook vengono eseguiti normalmente, in risposta agli eventi a cui sono legati. In quest'ultimo caso, l'hook riceve il contesto di legame (binding context) — dati formattati in JSON, di cui parleremo più avanti.

Creiamo l'operatore in Bash

Ora siamo pronti per l'implementazione. Per questo, dobbiamo scrivere due funzioni (ecco, raccomandiamo la libreria shell_lib, che semplifica notevolmente la scrittura di hook in Bash):

  • la prima è necessaria per la fase di configurazione — mostra il contesto di legame;
  • la seconda contiene la logica principale dell'hook.

#!/bin/bash

source /shell_lib.sh

function __config__() {
  cat << EOF
    configVersion: v1
    # BINDING CONFIGURATION
EOF
}

function __main__() {
  # THE LOGIC
}

hook::run "$@"

Il passo successivo è decidere quali oggetti ci servono. Nel nostro caso, è necessario monitorare:

  • la sorgente del segreto per eventuali modifiche;
  • tutti i namespace nel cluster, per sapere a quali di essi è attaccato l'etichetta;
  • i segreti obiettivo, per assicurarci che siano tutti sincronizzati con la sorgente del segreto.

Ci iscriviamo alla sorgente del segreto

La configurazione di binding è piuttosto semplice. Indichiamo che siamo interessati al Secret con il nome mysecret nello spazio dei nomi default:

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

function __config__() {
  cat << EOF
    configVersion: v1
    kubernetes:
    - name: src_secret
      apiVersion: v1
      kind: Secret
      nameSelector:
        matchNames:
        - mysecret
      namespace:
        nameSelector:
          matchNames: ["default"]
      group: main
EOF

Di conseguenza, il hook verrà attivato quando il secret di origine ({src_secret}) viene modificato e riceverà il seguente contesto di binding:src_secret) e riceverà il seguente context di binding:

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

Come potete vedere, contiene il nome e l'intero oggetto.

Monitoriamo gli spazi dei nomi

Ora dobbiamo iscriverti agli namespaces. Per fare ciò, indichiamo la seguente configurazione di binding:

- name: namespaces
  group: main
  apiVersion: v1
  kind: Namespace
  jqFilter: |
    {
      namespace: .metadata.name,
      hasLabel: (
       .metadata.labels // {} |  
         contains({"secret": "yes"})
      )
    }
  group: main
  keepFullObjectsInMemory: false

Come potete vedere, nella configurazione è apparso un nuovo campo chiamato jqFilter. Come suggerisce il suo nome, jqFilter filtra tutte le informazioni superflue e crea un nuovo oggetto JSON con i campi di nostro interesse. Un hook con una configurazione simile riceverà il seguente contesto di binding:

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

Contiene un array filterResults per ogni namespace nel cluster. La variabile booleana hasLabel mostra se l'etichetta è collegata a questo spazio dei nomi. Selettore keepFullObjectsInMemory: false indica che non è necessario mantenere oggetti completi in memoria.

Tracciamo i segreti obiettivo

Ci iscrivi a tutti i Secret che hanno un'annotazione impostata managed-secret: "yes" (questi sono i nostri obiettivi dst_secrets):

- name: dst_secrets
  apiVersion: v1
  kind: Secret
  labelSelector:
    matchLabels:
      managed-secret: "yes"
  jqFilter: |
    {
      "namespace":
        .metadata.namespace,
      "resourceVersion":
        .metadata.annotations.resourceVersion
    }
  group: main
  keepFullObjectsInMemory: false

In questo caso jqFilter filtra tutte le informazioni ad eccezione dello spazio dei nomi e del parametro resourceVersion. L'ultimo parametro è stato passato come annotazione durante la creazione del segreto: consente di confrontare le versioni dei segreti e mantenerle aggiornate.

Un hook configurato in questo modo, durante l'esecuzione, riceverà tre contesti di binding, descritti sopra. Possono essere visti come un tipo di istantanea (snapshot) del cluster.

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

Sulla base di tutte queste informazioni è possibile sviluppare un algoritmo di base. Esso itera su tutti gli spazi dei nomi e:

  • se hasLabel ha importanza true per lo spazio dei nomi corrente:
    • confronta il segreto globale con quello locale:
      • se sono identici — non fa nulla;
      • se sono diversi — esegue kubectl replace o create;
  • se hasLabel ha importanza false per lo spazio dei nomi corrente:
    • verifica che il Secret non sia presente in questo namespace:
      • se il Secret locale è presente, lo rimuove tramite kubectl delete;
      • se il Secret locale non viene trovato, non fa nulla.

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

Implementazione dell'algoritmo in Bash puoi scaricarlo dal nostro repository di esempi.

Ecco come siamo riusciti a creare un semplice controller Kubernetes, usando 35 righe di configurazione YAML e circa lo stesso numero di righe di codice in Bash! Il compito dello shell-operator è quello di unirli insieme.

Tuttavia, la copia dei segreti non è l'unico campo di applicazione dello strumento. Ecco altri esempi che dimostreranno di cosa è capace.

Esempio 1: apportare modifiche al ConfigMap

Consideriamo un Deployment composto da tre pod. I pod utilizzano il ConfigMap per memorizzare alcune configurazioni. Al momento dell'avvio, il ConfigMap si trovava in uno stato specifico (chiamiamolo v.1). Di conseguenza, tutti i pod utilizzano proprio questa versione del ConfigMap.

Ora supponiamo che il ConfigMap sia cambiato (v.2). Tuttavia, i pod continueranno a utilizzare la versione precedente del ConfigMap (v.1):

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

Come fare in modo che passino al nuovo ConfigMap (v.2)? La risposta è semplice: usare il template. Aggiungiamo un'annotazione con il checksum nella sezione template della configurazione del Deployment:

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

Di conseguenza, in tutti i pod sarà registrato questo checksum, e sarà lo stesso di quello del Deployment. Ora, è sufficiente aggiornare l'annotazione ogni volta che si modifica il ConfigMap. E il shell-operator è perfetto in questo caso. Tutto ciò che serve è programmare un hook che si iscriva al ConfigMap e aggiorni il checksum.

Se un utente apporta modifiche al ConfigMap, il shell-operator le rileverà e ricalcolerà il checksum. Dopo di che, entrerà in gioco la magia di Kubernetes: l'orchestratore ucciderà il pod, ne creerà uno nuovo, aspetterà finché non diventa Pronto, e passerà al successivo. Di conseguenza, il Deployment si sincronizzerà e passerà alla nuova versione del ConfigMap.

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

Esempio 2: lavoro con le Custom Resource Definitions

Come è noto, Kubernetes consente di creare tipi personalizzati (kinds) di oggetti. Ad esempio, si può creare un kind MysqlDatabase. Supponiamo che questo tipo abbia due parametri di metadata: name e namespace.

apiVersion: example.com/v1alpha1
kind: MysqlDatabase
metadata:
  name: foo
  namespace: bar

Abbiamo un cluster Kubernetes con vari spazi dei nomi, in cui possiamo creare database MySQL. In questo caso, il shell-operator può essere utilizzato per monitorare le risorse MysqlDatabase, le loro connessioni al server MySQL e la sincronizzazione dello stato desiderato e osservato del cluster.

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

Esempio 3: monitoraggio della rete del cluster

Come è noto, l'uso del ping è il modo più semplice per monitorare una rete. In questo esempio mostreremo come implementare un monitoraggio simile utilizzando il shell-operator.

Prima di tutto, sarà necessario iscriversi ai nodi. Il shell-operator ha bisogno del nome e dell'indirizzo IP di ogni nodo. Con queste informazioni, pingherà questi nodi.

configVersion: v1
kubernetes:
- name: nodes
  apiVersion: v1
  kind: Node
  jqFilter: |
    {
      name: .metadata.name,
      ip: (
       .status.addresses[] |  
        select(.type == "InternalIP") |
        .address
      )
    }
  group: main
  keepFullObjectsInMemory: false
  executeHookOnEvent: []
schedule:
- name: every_minute
  group: main
  crontab: "* * * * *"

Caratteristica executeHookOnEvent: [] prevenire l'esecuzione del hook in risposta a qualsiasi evento (cioè in risposta a modifiche, aggiunte, rimozioni di nodi). Tuttavia, esso verrà eseguito (e aggiornerà l'elenco dei nodi) secondo un programma — ogni minuto, come specificato dal campo schedule.

Ora sorge la domanda: come possiamo scoprire problemi come la perdita di pacchetti? Diamo un'occhiata al codice:

function __main__() {
  for i in $(seq 0 "$(context::jq -r '(.snapshots.nodes | length) - 1')"); do
    node_name="$(context::jq -r '.snapshots.nodes['"$i"'].filterResult.name')"
    node_ip="$(context::jq -r '.snapshots.nodes['"$i"'].filterResult.ip')"
    packets_lost=0
    if ! ping -c 1 "$node_ip" -t 1 ; then
      packets_lost=1
    fi
    cat >> "$METRICS_PATH" <<END
      {
        "name": "node_packets_lost",
        "add": $packets_lost,
        "labels": {
          "node": "$node_name"
        }
      }
END
  done
}

Iteriamo attraverso l'elenco dei nodi, otteniamo i loro nomi e indirizzi IP, li pinghiamo e inviamo i risultati a Prometheus. Il shell-operator può esportare metriche in Prometheus, memorizzandole in un file situato secondo il percorso specificato nella variabile d'ambiente $METRICS_PATH.

Ecco come si può creare un operatore per un semplice monitoraggio di rete nel cluster.

Meccanismo delle code

Questo articolo sarebbe incompleto senza descrivere un altro meccanismo importante integrato nel shell-operator. Immagina che esegua un certo hook in risposta a un evento nel cluster.

  • Cosa succede se nel cluster si verifica un altro evento?
  • Il shell-operator avvierà un'altra istanza dell'hook?
  • E se nel cluster si verificassero, diciamo, cinque eventi contemporaneamente?
  • Il shell-operator elaborerà questi in parallelo?
  • E riguardo alle risorse consumate, come memoria e CPU?

Fortunatamente, il shell-operator ha un meccanismo di queue integrato. Tutti gli eventi vengono messi in coda e elaborati in sequenza.

Illustriamo questo con alcuni esempi. Supponiamo di avere due hook. Il primo evento viene assegnato al primo hook. Dopo che la sua elaborazione è completata, la coda avanza. I tre eventi successivi vengono reindirizzati al secondo hook — vengono estratti dalla coda e consegnati in "batch". Cioè l'hook riceve un array di eventi — o, più precisamente, un array di contesti di binding.

Inoltre, questi eventi possono essere uniti in un unico grande. Questo è controllato dal parametro gruppo nella configurazione di binding.

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

È possibile creare un numero qualsiasi di code/hook e le loro combinazioni. Per esempio, una coda può lavorare con due hook, o viceversa.

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

Tutto ciò che è necessario fare è configurare correttamente il campo queue nella configurazione di binding. Se non viene specificato alcun nome per la coda, l'hook viene avviato nella coda predefinita (default). Un meccanismo di coda del genere permette di risolvere completamente tutti i problemi di gestione delle risorse quando si lavora con i hook.

Conclusione

Abbiamo spiegato cos'è il shell-operator, mostrando come utilizzarlo per creare rapidamente e senza troppi sforzi operatori Kubernetes, e fornito diversi esempi di utilizzo.

Informazioni dettagliate sul shell-operator e una guida breve sul suo utilizzo sono disponibili nel corrispondente repository su GitHub. Non esitate a contattarci con domande: potete discuterne nel nostro gruppo Telegram (in russo) o in questo forum (in inglese).

E se vi è piaciuto, siamo sempre felici di nuovi issues/PR/stelle su GitHub, dove, a proposito, si possono trovare anche altri progetti interessanti. Tra questi spicca addon-operator, che è il "fratello maggiore" del shell-operator. Questo strumento utilizza i chart Helm per installare addon, è in grado di fornire aggiornamenti e monitorare vari parametri/valori dei chart, controlla il processo di installazione dei chart e può anche modificarli in risposta a eventi nel cluster.

Go? Bash! Presentiamo shell-operator (overview e video della presentazione da KubeCon EU'2020)

Video e slide

Un video della presentazione (~23 minuti):

Riproduci video

Presentazione della relazione:

P.S.

Leggete anche nel nostro blog:

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster