andare? Bash! Incontra l'operatore di shell (recensione e video talk da KubeCon EU'2020)

Quest'anno la principale conferenza europea di Kubernetes - KubeCon + CloudNativeCon Europe 2020 - è stata virtuale. Tuttavia, un tale cambiamento di formato non ci ha impedito di consegnare il nostro rapporto pianificato da tempo “Go? Bash! Meet the Shell-operator” dedicato al nostro progetto Open Source operatore di shell.

Questo articolo, ispirato alla conferenza, presenta un approccio per semplificare il processo di creazione degli operatori per Kubernetes e mostra come crearne uno tuo con il minimo sforzo utilizzando un operatore di shell.

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

Introduzione video del resoconto (~23 minuti in inglese, notevolmente più informativo dell'articolo) e il suo estratto principale in forma testuale. Andare!

In Flant ottimizziamo e automatizziamo costantemente tutto. Oggi parleremo di un altro concetto entusiasmante. Incontrare: scripting di shell nativo del cloud!

Partiamo però dal contesto in cui tutto ciò avviene: Kubernetes.

API e controller Kubernetes

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

  • per ricevere risorsa per tipo e nome;
  • cambiare risorsa (in questo caso, il server memorizza solo oggetti "corretti" - tutti quelli formati in modo errato o destinati ad altre directory vengono scartati);
  • seguire per la risorsa (in questo caso, l'utente riceve immediatamente la sua versione corrente/aggiornata).

Pertanto, Kubernetes agisce come una sorta di file server (per i manifest YAML) con tre metodi di base (sì, in realtà ce ne sono altri, ma per ora li ometteremo).

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

Il problema è che il server può solo archiviare informazioni. Per farlo funzionare è necessario controllore - il secondo concetto più importante e fondamentale nel mondo di Kubernetes.

Esistono due tipi principali di controller. Il primo prende le informazioni da Kubernetes, le elabora secondo una logica annidata e le restituisce a K8s. Il secondo prende informazioni da Kubernetes, ma, a differenza del primo tipo, modifica lo stato di alcune risorse esterne.

Diamo uno sguardo più da vicino al processo di creazione di una distribuzione in Kubernetes:

  • Controller di distribuzione (incluso in kube-controller-manager) riceve informazioni sulla distribuzione e crea un ReplicaSet.
  • ReplicaSet crea due repliche (due pod) in base a queste informazioni, ma questi pod non sono ancora pianificati.
  • Lo scheduler pianifica i pod e aggiunge informazioni sui nodi ai relativi YAML.
  • I Kubelet apportano modifiche a una risorsa esterna (ad esempio Docker).

Quindi l'intera sequenza viene ripetuta in ordine inverso: il kubelet controlla i contenitori, calcola lo stato del pod e lo rimanda indietro. Il controller ReplicaSet riceve lo stato e aggiorna lo stato del set di repliche. La stessa cosa accade con il Deployment Controller e l'utente ottiene finalmente lo stato aggiornato (corrente).

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

Operatore di shell

Si scopre che Kubernetes si basa sul lavoro congiunto di vari controller (gli operatori Kubernetes sono anche controller). La domanda sorge spontanea: come creare il proprio operatore con il minimo sforzo? E qui ci viene in soccorso quello che abbiamo sviluppato operatore di shell. Consente agli amministratori di sistema di creare le proprie dichiarazioni utilizzando metodi familiari.

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

Esempio semplice: copiare i segreti

Diamo un'occhiata a un semplice esempio.

Supponiamo di avere un cluster Kubernetes. Ha uno spazio dei nomi default con qualche segreto mysecret. Inoltre, nel cluster sono presenti altri spazi dei nomi. Ad alcuni di essi è attaccata un'etichetta specifica. Il nostro obiettivo è copiare Secret negli spazi dei nomi con un'etichetta.

Il compito è complicato dal fatto che nel cluster potrebbero apparire nuovi spazi dei nomi e alcuni di essi potrebbero avere questa etichetta. D'altra parte, quando l'etichetta viene eliminata, anche Secret dovrebbe essere eliminato. Oltre a questo, anche il Secret stesso può cambiare: in questo caso, il nuovo Secret dovrà essere copiato in tutti i namespace dotati di etichette. Se Secret viene eliminato accidentalmente in qualsiasi spazio dei nomi, il nostro operatore dovrebbe ripristinarlo immediatamente.

Ora che l'attività è stata formulata, è il momento di iniziare a implementarla utilizzando l'operatore di shell. Ma prima vale la pena spendere qualche parola sull’operatore shell stesso.

Come funziona l'operatore shell

Come altri carichi di lavoro in Kubernetes, Shell-Operator viene eseguito nel proprio pod. In questo pod nella directory /hooks vengono memorizzati i file eseguibili. Questi possono essere script in Bash, Python, Ruby, ecc. Chiamiamo questi file eseguibili hook (ganci).

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

L'operatore shell si iscrive agli eventi Kubernetes ed esegue questi hook in risposta agli eventi di cui abbiamo bisogno.

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

Come fa l'operatore shell a sapere quale hook eseguire e quando? Il punto è che ogni hook ha due fasi. Durante l'avvio, l'operatore shell esegue tutti gli hook con un argomento --config Questa è la fase di configurazione. E dopo, i ganci vengono lanciati normalmente, in risposta agli eventi a cui sono collegati. In quest'ultimo caso, l'hook riceve il contesto vincolante (contesto vincolante) - dati in formato JSON, di cui parleremo più in dettaglio di seguito.

Creare un operatore in Bash

Ora siamo pronti per l'implementazione. Per fare ciò, dobbiamo scrivere due funzioni (a proposito, consigliamo biblioteca shell_lib, che semplifica notevolmente la scrittura degli hook in Bash):

  • il primo è necessario per la fase di configurazione: visualizza il contesto vincolante;
  • il secondo 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 prossimo passo è decidere di quali oggetti abbiamo bisogno. Nel nostro caso, dobbiamo monitorare:

  • segreto di origine per le modifiche;
  • tutti gli spazi dei nomi nel cluster, in modo da sapere a quali è associata un'etichetta;
  • target secret per garantire che siano tutti sincronizzati con il segreto di origine.

Iscriviti alla fonte segreta

La configurazione vincolante è abbastanza semplice. Indichiamo che siamo interessati a Secret con il nome mysecret nello spazio dei nomi default:

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video 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, l'hook verrà attivato quando il segreto di origine cambia (src_secret) e ricevere il seguente contesto vincolante:

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

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

Tenere traccia degli spazi dei nomi

Ora devi iscriverti agli spazi dei nomi. Per fare ciò, specifichiamo la seguente configurazione di associazione:

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

Come puoi vedere, nella configurazione è apparso un nuovo campo con il nome jqFilter. Come suggerisce il nome, jqFilter filtra tutte le informazioni non necessarie e crea un nuovo oggetto JSON con i campi che ci interessano. Un hook con una configurazione simile riceverà il seguente contesto vincolante:

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

Contiene un array filterResults per ogni spazio dei nomi nel cluster. Variabile booleana hasLabel indica se un'etichetta è allegata a un determinato spazio dei nomi. Selettore keepFullObjectsInMemory: false indica che non è necessario mantenere in memoria oggetti completi.

Tracciamento dei segreti del bersaglio

Sottoscriviamo tutti i segreti per cui è specificata un'annotazione 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 tranne lo spazio dei nomi e il parametro resourceVersion. L'ultimo parametro è stato passato all'annotazione durante la creazione del segreto: consente di confrontare le versioni dei segreti e mantenerli aggiornati.

Un hook configurato in questo modo, una volta eseguito, riceverà i tre contesti di associazione descritti sopra. Possono essere pensati come una sorta di istantanea (istantanea) grappolo.

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

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

  • se hasLabel questioni true per lo spazio dei nomi corrente:
    • confronta il segreto globale con quello locale:
      • se sono uguali, non fa nulla;
      • se differiscono, viene eseguito kubectl replace o create;
  • se hasLabel questioni false per lo spazio dei nomi corrente:
    • si assicura che Secret non sia nello spazio dei nomi specificato:
      • se è presente il Secret locale, cancellatelo utilizzando kubectl delete;
      • se il Secret locale non viene rilevato, non fa nulla.

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

Implementazione dell'algoritmo in Bash puoi scaricare nel nostro repository con esempi.

È così che siamo riusciti a creare un semplice controller Kubernetes utilizzando 35 righe di configurazione YAML e circa la stessa quantità di codice Bash! Il compito dell'operatore shell è collegarli insieme.

Tuttavia, la copia dei segreti non è l'unico campo di applicazione dell'utilità. Ecco qualche altro esempio per mostrare di cosa è capace.

Esempio 1: apportare modifiche a ConfigMap

Diamo un'occhiata a un Deployment composto da tre pod. I pod utilizzano ConfigMap per archiviare alcune configurazioni. Quando i pod sono stati lanciati, ConfigMap era in un certo stato (chiamiamolo v.1). Di conseguenza, tutti i pod utilizzano questa particolare versione di ConfigMap.

Ora supponiamo che ConfigMap sia cambiato (v.2). Tuttavia, i pod utilizzeranno la versione precedente di ConfigMap (v.1):

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

Come posso convincerli a passare alla nuova ConfigMap (v.2)? La risposta è semplice: usa un modello. Aggiungiamo un'annotazione di checksum alla sezione template Configurazioni di distribuzione:

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

Di conseguenza, questo checksum verrà registrato in tutti i pod e sarà uguale a quello di Deployment. Ora devi solo aggiornare l'annotazione quando la ConfigMap cambia. E l'operatore shell torna utile in questo caso. Tutto quello che devi fare è programmare un hook che si iscriverà a ConfigMap e aggiornerà il checksum.

Se l'utente apporta modifiche al ConfigMap, l'operatore shell le noterà e ricalcolerà il checksum. Dopodiché entrerà in gioco la magia di Kubernetes: l'orchestratore ucciderà il pod, ne creerà uno nuovo, attenderà che diventi Ready, e passa a quello successivo. Di conseguenza, la distribuzione verrà sincronizzata e passerà alla nuova versione di ConfigMap.

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

Esempio 2: utilizzo delle definizioni di risorse personalizzate

Come sai, Kubernetes ti consente di creare tipi di oggetti personalizzati. Ad esempio, puoi creare kind MysqlDatabase. Diciamo che questo tipo ha due parametri di metadati: name и namespace.

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

Disponiamo di un cluster Kubernetes con diversi spazi dei nomi in cui possiamo creare database MySQL. In questo caso l'operatore shell può essere utilizzato per tenere traccia delle risorse MysqlDatabase, collegandoli al server MySQL e sincronizzando gli stati desiderati e osservati del cluster.

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

Esempio 3: monitoraggio della rete del cluster

Come sai, usare il ping è il modo più semplice per monitorare una rete. In questo esempio mostreremo come implementare tale monitoraggio utilizzando shell-operator.

Prima di tutto, dovrai iscriverti ai nodi. L'operatore shell necessita del nome e dell'indirizzo IP di ciascun nodo. Con il loro aiuto, eseguirà il ping di 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: "* * * * *"

Parametro executeHookOnEvent: [] impedisce all'hook di essere eseguito in risposta a qualsiasi evento (ovvero, in risposta alla modifica, aggiunta, eliminazione di nodi). Tuttavia, lui correrà (e aggiorna l'elenco dei nodi) In programma - ogni minuto, come prescritto dal campo schedule.

Ora sorge la domanda: come facciamo a sapere esattamente di 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
}

Esaminiamo l'elenco dei nodi, otteniamo i loro nomi e indirizzi IP, li eseguiamo il ping e inviamo i risultati a Prometheus. L'operatore shell può esportare le metriche in Prometheus, salvandoli in un file posizionato in base al percorso specificato nella variabile di ambiente $METRICS_PATH.

Ti piace questa puoi creare un operatore per il semplice monitoraggio della rete in un cluster.

Meccanismo di coda

Questo articolo sarebbe incompleto senza descrivere un altro importante meccanismo integrato nell'operatore di shell. Immagina che esegua una sorta di hook in risposta a un evento nel cluster.

  • Cosa succede se, contemporaneamente, succede qualcosa nel cluster? un'altra cosa evento?
  • L'operatore shell eseguirà un'altra istanza dell'hook?
  • Cosa succederebbe se, ad esempio, si verificassero cinque eventi contemporaneamente nel cluster?
  • L'operatore shell li elaborerà in parallelo?
  • E le risorse consumate come memoria e CPU?

Fortunatamente, l'operatore shell ha un meccanismo di accodamento integrato. Tutti gli eventi vengono accodati ed elaborati in sequenza.

Illustriamolo con degli esempi. Diciamo che abbiamo due ganci. Il primo evento va al primo hook. Una volta completata l'elaborazione, la coda avanza. I successivi tre eventi vengono reindirizzati al secondo hook: vengono rimossi dalla coda e inseriti in un "pacchetto". Questo è hook riceve una serie di eventi – o, più precisamente, una serie di contesti vincolanti.

Anche questi gli eventi possono essere combinati in un unico grande. Il parametro è responsabile di ciò group nella configurazione vincolante.

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

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

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

Tutto quello che devi fare è configurare il campo di conseguenza queue nella configurazione vincolante. Se non viene specificato un nome di coda, l'hook viene eseguito sulla coda predefinita (default). Questo meccanismo di accodamento consente di risolvere completamente tutti i problemi di gestione delle risorse quando si lavora con gli hook.

conclusione

Abbiamo spiegato cos'è un operatore di shell, mostrato come può essere utilizzato per creare operatori Kubernetes in modo rapido e semplice e fornito diversi esempi del suo utilizzo.

Informazioni dettagliate sull'operatore shell, nonché un breve tutorial su come utilizzarlo, sono disponibili nel file corrispondente repository su GitHub. Non esitate a contattarci per eventuali domande: potrete discuterne in uno speciale Gruppo Telegram (in russo) o in questo forum (in inglese).

E se ti è piaciuto, siamo sempre felici di vedere nuovi numeri/PR/star su GitHub, dove tra l'altro ne puoi trovare altri progetti interessanti. Tra questi vale la pena evidenziare operatore-add-on, che è il fratello maggiore di shell-operator. Questa utilità utilizza i grafici Helm per installare componenti aggiuntivi, può fornire aggiornamenti e monitorare vari parametri/valori dei grafici, controlla il processo di installazione dei grafici e può anche modificarli in risposta agli eventi nel cluster.

Andare? Bash! Incontra l'operatore shell (recensione e rapporto video da KubeCon EU'2020)

Video e diapositive

Video dallo spettacolo (~23 minuti):


Presentazione del rapporto:

PS

Leggi anche sul nostro blog:

Fonte: habr.com

Aggiungi un commento