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

Quest'anno, la principale conferenza europea su Kubernetes — KubeCon + CloudNativeCon Europe 2020 — si è tenuta in modalità virtuale. Tuttavia, questo cambio di formato non ci ha impedito di tenere un intervento da tempo pianificato, intitolato "Go? Bash! Meet the Shell-operator", dedicato al nostro progetto Open Source. operatore di shell.

Questo articolo, basato sulla presentazione, presenta un approccio per semplificare il processo di creazione di operatori per Kubernetes e mostra come è possibile crearne di propri con il minimo sforzo utilizzando shell-operator.

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di KubeCon EU'2020)

Introduzione video del resoconto (~23 minuti in inglese, molto più informativo dell'articolo) e il riassunto principale in formato testo. Andiamo!

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

Ma cominciamo dal contesto in cui tutto questo accade: Kubernetes.

API e controller Kubernetes

L'API di Kubernetes può essere considerata come un file server con directory per ogni tipo di oggetto. Gli oggetti (risorse) su questo server sono rappresentati da file YAML. Inoltre, il server dispone di un'API di base che consente di eseguire tre operazioni:

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

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

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di KubeCon EU'2020)

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

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

Diamo un'occhiata più da vicino al processo di creazione di una distribuzione in Kubernetes:

  • Controller di distribuzione (incluso in kube-controller-manager) ottiene 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 loro YAML.
  • I kubelet apportano modifiche a una risorsa esterna (ad esempio, Docker).

Quindi l'intera sequenza viene ripetuta al contrario: kubelet controlla i container, calcola lo stato del pod e lo invia. ReplicaSet Controller riceve lo stato e aggiorna lo stato del replica set. Lo stesso accade con Deployment Controller e l'utente riceve infine lo stato aggiornato (corrente).

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di KubeCon EU'2020)

Operatore di shell

A quanto pare, Kubernetes si basa sul lavoro congiunto di diversi controller (anche gli operatori di Kubernetes sono controller). La domanda sorge spontanea: come creare un proprio operatore con il minimo sforzo? Ed ecco che ci viene in soccorso quello che abbiamo sviluppato. operatore di shellConsente agli amministratori di sistema di creare i propri operatori utilizzando metodi familiari.

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di KubeCon EU'2020)

Esempio semplice: copia dei segreti

Diamo un'occhiata a un semplice esempio.

Supponiamo di avere un cluster Kubernetes. Ha uno spazio dei nomi default con qualche segreto mysecretInoltre, nel cluster sono presenti altri namespace. Ad alcuni di essi è associata un'etichetta specifica. Il nostro obiettivo è copiare Secret nei namespace con l'etichetta.

Il compito è complicato dal fatto che nuovi namespace potrebbero apparire nel cluster, e alcuni di essi potrebbero avere questa etichetta. D'altra parte, quando l'etichetta viene eliminata, anche il Segreto deve essere eliminato. Inoltre, anche il Segreto stesso potrebbe cambiare: in questo caso, il nuovo Segreto deve essere copiato in tutti i namespace con etichette. Se un Segreto viene eliminato accidentalmente in un qualsiasi namespace, il nostro operatore deve ripristinarlo immediatamente.

Ora che il compito è stato formulato, è il momento di iniziare a implementarlo utilizzando shell-operator. Ma prima, qualche parola su shell-operator stesso.

Come funziona l'operatore shell

Come altri carichi di lavoro Kubernetes, shell-operator viene eseguito nel proprio pod. In quel pod, nella directory /hooks vengono memorizzati file eseguibili. Possono essere script in Bash, Python, Ruby, ecc. Chiamiamo tali file eseguibili hook (ganci).

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di KubeCon EU'2020)

Shell-operator si iscrive agli eventi Kubernetes ed esegue questi hook in risposta agli eventi di cui abbiamo bisogno.

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di 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. Successivamente, gli hook vengono lanciati nel modo consueto, in risposta agli eventi a cui sono vincolati. In quest'ultimo caso, l'hook riceve un contesto di vincolo (contesto vincolante) - dati in formato JSON, di cui parleremo più dettagliatamente di seguito.

Creazione di un operatore in Bash

Ora siamo pronti per l'implementazione. Per farlo, dovremo scrivere due funzioni (a proposito, consigliamo biblioteca libreria_shell, che semplifica notevolmente la scrittura di hook in Bash):

  • il primo è necessario per la fase di configurazione: restituisce il contesto di binding;
  • Il secondo contiene la logica principale del gancio.

#!/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 di quali oggetti abbiamo bisogno. Nel nostro caso, dobbiamo tracciare:

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

Iscriviti alla fonte segreta

La configurazione del binding è piuttosto semplice. Specifichiamo che siamo interessati a Secret con il nome mysecret nello spazio dei nomi default:

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di 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 gancio verrà attivato quando cambia il segreto sorgente (src_secret) e ottenere il seguente contesto di binding:

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di KubeCon EU'2020)

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

Tenere traccia degli spazi dei nomi

Ora dobbiamo sottoscrivere gli spazi dei nomi. Per farlo, specifichiamo 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 puoi vedere, nella configurazione è apparso un nuovo campo denominato jqFilter. Come suggerisce il nome, jqFilter Filtra tutte le informazioni non necessarie e crea un nuovo oggetto JSON con i campi di nostro interesse. Un hook con questa configurazione riceverà il seguente contesto di binding:

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di KubeCon EU'2020)

Contiene una matrice filterResults per ogni namespace nel cluster. Variabile booleana hasLabel indica se l'etichetta è associata allo spazio dei nomi specificato. Selettore keepFullObjectsInMemory: false indica che non è necessario mantenere oggetti completi in memoria.

Tracciamento di obiettivi segreti

Ci iscriviamo a tutti i Segreti che hanno un set di annotazioni 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 resourceVersionL'ultimo parametro è stato passato all'annotazione quando è stato creato il segreto: consente di confrontare le versioni dei segreti e di mantenerle aggiornate.

Un hook configurato in questo modo, una volta eseguito, riceverà i tre contesti di binding descritti sopra. Puoi considerarli come una sorta di snapshot (istantanea) grappolo.

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di KubeCon EU'2020)

Sulla base di tutte queste informazioni, è possibile sviluppare un algoritmo di base. Questo algoritmo itera attraverso 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 - esegue kubectl replace o create;
  • se hasLabel questioni false per lo spazio dei nomi corrente:
    • assicura che Secret non si trovi nello spazio dei nomi specificato:
      • se è presente un segreto locale, lo elimina utilizzando kubectl delete;
      • se il segreto locale non viene trovato, non fa nulla.

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di KubeCon EU'2020)

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

Ecco come siamo riusciti a creare un semplice controller Kubernetes utilizzando 35 righe di configurazioni YAML e circa la stessa quantità di codice Bash! Il compito dell'operatore shell è quello di collegarli tra loro.

Tuttavia, la copia dei segreti non è l'unico ambito di applicazione dell'utilità. Ecco alcuni altri esempi che mostreranno le sue potenzialità.

Esempio 1: apportare modifiche a ConfigMap

Consideriamo un Deployment composto da tre pod. I pod utilizzano una ConfigMap per memorizzare alcune configurazioni. All'avvio dei pod, la ConfigMap si trovava in uno stato (chiamiamolo v.1). Di conseguenza, tutti i pod utilizzano questa versione della ConfigMap.

Supponiamo ora che la ConfigMap sia cambiata (v.2). Tuttavia, i pod continueranno a utilizzare la vecchia versione della ConfigMap (v.1):

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di KubeCon EU'2020)

Come possiamo farli passare alla nuova ConfigMap (v.2)? La risposta è semplice: usare un modello. Aggiungiamo un'annotazione di checksum alla sezione template Configurazioni di distribuzione:

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di KubeCon EU'2020)

Di conseguenza, tutti i pod avranno questo checksum, che sarà lo stesso del Deployment. Ora non vi resta che aggiornare l'annotazione quando cambia la ConfigMap. E l'operatore shell è molto utile in questo caso. Tutto ciò che dovete 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é, la magia di Kubernetes entrerà in gioco: l'orchestratore eliminerà il pod, ne creerà uno nuovo e attenderà che diventi Readye passa a quello successivo. Di conseguenza, la distribuzione verrà sincronizzata e spostata alla nuova versione di ConfigMap.

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di KubeCon EU'2020)

Esempio 2: lavorare con definizioni di risorse personalizzate

Come sapete, Kubernetes consente di creare tipi (generi) personalizzati di oggetti. Ad esempio, è possibile creare un tipo MysqlDatabaseSupponiamo che questo tipo abbia due parametri di metadati: name и namespace.

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

Abbiamo un cluster Kubernetes con diversi namespace in cui possiamo creare database MySQL. In questo caso, l'operatore shell può essere utilizzato per monitorare le risorse. MysqlDatabase, la loro connessione al server MySQL e la sincronizzazione degli stati desiderati e osservati del cluster.

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di KubeCon EU'2020)

Esempio 3: Monitoraggio di una rete di cluster

Come sapete, usare il ping è il modo più semplice per monitorare una rete. In questo esempio, mostreremo come implementare tale monitoraggio utilizzando l'operatore shell.

Per prima cosa, è necessario sottoscrivere i nodi. L'operatore shell necessita del nome e dell'indirizzo IP di ciascun nodo. Li userà per effettuare il ping di tali 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 che l'hook venga attivato in risposta a qualsiasi evento (ad esempio in risposta a nodi modificati, aggiunti o rimossi). Tuttavia, sarà lanciato (e aggiornare l'elenco dei nodi) In programma - ogni minuto, come prescrive il campo schedule.

Ora sorge spontanea 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, ne otteniamo i nomi e gli indirizzi IP, li pingiamo e inviamo i risultati a Prometheus. L'operatore Shell può esportare le metriche su Prometheus, salvandoli in un file posizionato secondo il percorso specificato nella variabile d'ambiente $METRICS_PATH.

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

Meccanismo di coda

Questo articolo non sarebbe completo senza descrivere un altro importante meccanismo integrato in shell-operator. Immaginate che esegua un hook in risposta a un evento nel cluster.

  • Cosa succede se contemporaneamente si verifica un problema nel cluster? un'altra cosa evento?
  • Shell-operator eseguirà un'altra istanza dell'hook?
  • Cosa succederebbe se, per esempio, in un cluster si verificassero contemporaneamente cinque eventi?
  • L'operatore shell li elaborerà in parallelo?
  • E che dire delle risorse consumate, come memoria e CPU?

Fortunatamente, shell-operator ha un meccanismo di accodamento integrato. Tutti gli eventi vengono accodati ed elaborati in sequenza.

Illustriamolo con degli esempi. Supponiamo di avere due hook. Il primo evento viene inoltrato al primo hook. Una volta completata l'elaborazione, la coda avanza. I tre eventi successivi vengono inoltrati al secondo hook: vengono estratti dalla coda e inviati ad esso in un "batch". In altre parole, l'hook riceve una serie di eventi — o, più precisamente, una serie di contesti vincolanti.

Anche questi gli eventi possono essere combinati in uno grandeIl parametro è responsabile di questo. group nella configurazione di rilegatura.

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di KubeCon EU'2020)

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

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di KubeCon EU'2020)

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

conclusione

Abbiamo spiegato cos'è shell-operator, mostrato come può essere utilizzato per creare rapidamente e facilmente operatori Kubernetes e fornito diversi esempi del suo utilizzo.

Informazioni dettagliate sull'operatore shell, nonché una guida rapida su come utilizzarlo, sono disponibili nel corrispondente repository su GitHubNon esitate a contattarci per qualsiasi domanda: potrete discuterne in un apposito spazio Gruppo Telegram (in russo) o in questo forum (in inglese).

E se ti è piaciuto, siamo sempre felici di vedere nuovi problemi/PR/stelle su GitHub, dove, tra l'altro, puoi trovarne altri progetti interessantiTra questi, vale la pena sottolineare operatore-add-on, che è il fratello maggiore di shell-operatorQuesta utility 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.

Pronti? Bash! Incontra Shell-Operator (recensione e video del talk di KubeCon EU'2020)

Video e diapositive

Video dell'esibizione (~23 minuti):

Guarda il video

Presentazione del rapporto:

PS

Leggi anche sul nostro blog:

Fonte: habr.com