La nostra esperienza con i dati nel cluster Kubernetes etcd direttamente (senza K8s API)

Sempre più spesso i clienti ci contattano chiedendo di garantire l'accesso al cluster Kubernetes per poter interagire con i servizi all'interno del cluster: per connettersi direttamente a un database o a un servizio, per collegare un'applicazione locale alle applicazioni all'interno del cluster…

La nostra esperienza con i dati nel cluster Kubernetes etcd direttamente (senza K8s API)

Ad esempio, potrebbe sorgere la necessità di collegarsi dal proprio computer locale al servizio memcached.staging.svc.cluster.local. Offriamo questa possibilità tramite VPN all'interno del cluster a cui si connette il cliente. Per questo annunciamo le sotto-reti dei pod, dei servizi e inoltriamo il DNS del cluster al cliente. In questo modo, quando il cliente tenta di connettersi al servizio memcached.staging.svc.cluster.local, la richiesta va al DNS del cluster e in risposta riceve l'indirizzo di quel servizio dalla rete dei servizi del cluster o l'indirizzo di un pod.

Configuramos i cluster K8s utilizzando kubeadm, dove per impostazione predefinita la sotto-rete dei servizi è 192.168.0.0/16, e la rete dei pod è 10.244.0.0/16. Di solito tutto funziona bene, ma ci sono un paio di aspetti:

  • La sotto-rete 192.168.*.* è spesso utilizzata nelle reti aziendali dei clienti, e ancor più spesso nelle reti domestiche degli sviluppatori. E quindi ci troviamo di fronte a conflitti: i router domestici operano in questa sotto-rete e la VPN inoltra queste sotto-reti dal cluster al cliente.
  • Abbiamo diversi cluster (cluster di produzione, staging e/o diversi cluster di sviluppo). Quindi in tutti loro, per impostazione predefinita, ci saranno sottoreti identiche per i pod e i servizi, il che crea grandi difficoltà per lavorare contemporaneamente con i servizi in più cluster.

Da tempo abbiamo adottato la prassi di utilizzare sottoreti diverse per i servizi e i pod all'interno di un progetto — in generale, affinché tutti i cluster siano con reti diverse. Tuttavia, ci sono un gran numero di cluster in produzione che non vogliamo ricreare da zero, poiché in essi sono attivati molti servizi, applicazioni stateful, ecc.

E allora ci siamo posti la domanda: come possiamo cambiare la sotto-rete in un cluster esistente?

Ricerca di soluzioni

La prassi più comune è quella di ricreare tutti servizi di tipo ClusterIP. Come alternativa, potrebbero consigliare e così via:

Il seguente processo presenta un problema: dopo che tutto è stato configurato, i pod si avviano con l'IP precedente come nameserver DNS in /etc/resolv.conf.
Poiché non ho ancora trovato la soluzione, ho dovuto resettare l'intero cluster con kubeadm reset e inizializzarlo di nuovo.

Ma non è adatto a tutti… Ecco ulteriori dettagli per il nostro caso:

  • Si utilizza Flannel;
  • Ci sono cluster sia nel cloud che su hardware;
  • Ci piacerebbe evitare il ridispiegamento di tutti i servizi nel cluster;
  • C'è la necessità di fare tutto con il minor numero possibile di problemi;
  • La versione di Kubernetes è 1.16.6 (tuttavia, le azioni successive saranno analoghe anche per altre versioni);
  • Il compito principale è quello di sostituire nella rete di servizio del cluster, distribuito con kubeadm 192.168.0.0/16, con 172.24.0.0/16.

E così è accaduto che eravamo da tempo interessati a vedere cosa e come viene memorizzato in Kubernetes in etcd, e cosa si può fare con questo... Abbiamo pensato: "Perché non aggiornare semplicemente i dati in etcd, sostituendo i vecchi indirizzi IP (rete) con i nuovi?

Cercando strumenti pronti per lavorare con i dati in etcd, non abbiamo trovato nulla che risolvesse completamente il compito porposto. (A proposito, se conoscete qualsiasi strumento per lavorare direttamente con i dati in etcd, saremo grati se ci inviate i link.) Tuttavia, un buon punto di partenza è stato etcdhelper di OpenShift (grazie ai suoi autori!).

Questo strumento è in grado di connettersi a etcd utilizzando certificati e di leggere i dati da lì utilizzando i comandi ls, get, dump.

Aggiungiamo etcdhelper

Il pensiero successivo è naturale: "Cosa impedisce di aggiungere a questo strumento la possibilità di scrivere dati in etcd?"

Questo si è concretizzato in una versione modificata di etcdhelper con due nuove funzioni changeServiceCIDR e changePodCIDR. Puoi vedere il suo codice qui.

Cosa fanno le nuove funzioni? L'algoritmo changeServiceCIDR:

  • crea un deserializzatore;
  • compila un'espressione regolare per sostituire CIDR;
  • scorre tutti i servizi con tipo ClusterIP nel cluster:
    • decodifica il valore da etcd in un oggetto Go;
    • utilizza un'espressione regolare per sostituire i primi due byte dell'indirizzo;
    • assegna al servizio un indirizzo IP della nuova rete;
    • crea un serializzatore, trasforma l'oggetto Go in protobuf, scrive i nuovi dati in etcd.

La funzione changePodCIDR è sostanzialmente analoga changeServiceCIDR – solo invece di modificare le specifiche dei servizi lo facciamo per il nodo e cambiamo .spec.PodCIDR in una nuova rete.

Pratica

Cambio di serviceCIDR

Il piano per l'attuazione del compito proposto è molto semplice, ma implica un downtime al momento della ricreazione di tutti i pod nel cluster. Dopo aver descritto i passaggi principali, condivideremo anche le riflessioni su come in teoria sia possibile ridurre al minimo questo downtime.

Azioni preliminari:

  • installazione del software necessario e costruzione del etcdhelper patchato;
  • backup di etcd e /etc/kubernetes.

Piano d'azione riassuntivo per cambiare serviceCIDR:

  • modifica dei manifesti dell'apiserver e del controller-manager;
  • riemissione dei certificati;
  • modifica dei servizi ClusterIP in etcd;
  • riavvio di tutti i pod nel cluster.

Di seguito è riportata la sequenza completa delle azioni in dettaglio.

1. Installiamo etcd-client per il dump dei dati:

apt install etcd-client

2. Compiliamo etcdhelper:

  • Installiamo golang:
    GOPATH=/root/golang
    mkdir -p $GOPATH/local
    curl -sSL https://dl.google.com/go/go1.14.1.linux-amd64.tar.gz | tar -xzvC $GOPATH/local
    echo "export GOPATH="$GOPATH"" >> ~/.bashrc
    echo 'export GOROOT="$GOPATH/local/go"' >> ~/.bashrc
    echo 'export PATH="$PATH:$GOPATH/local/go/bin"' >> ~/.bashrc
  • Salviamo etcdhelper.go, carichiamo le dipendenze, compiliamo:
    wget https://raw.githubusercontent.com/flant/examples/master/2020/04-etcdhelper/etcdhelper.go
    go get go.etcd.io/etcd/clientv3 k8s.io/kubectl/pkg/scheme k8s.io/apimachinery/pkg/runtime
    go build -o etcdhelper etcdhelper.go

3. Facciamo un backup di etcd:

backup_dir=/root/backup
mkdir ${backup_dir}
cp -rL /etc/kubernetes ${backup_dir}
ETCDCTL_API=3 etcdctl --cacert=/etc/kubernetes/pki/etcd/ca.crt --key=/etc/kubernetes/pki/etcd/server.key --cert=/etc/kubernetes/pki/etcd/server.crt --endpoints https://192.168.199.100:2379 snapshot save ${backup_dir}/etcd.snapshot

4. Modifichiamo la rete dei servizi nei manifesti del controllo del piano Kubernetes. Nei file /etc/kubernetes/manifests/kube-apiserver.yaml e /etc/kubernetes/manifests/kube-controller-manager.yaml modifichiamo il parametro --service-cluster-ip-range alla nuova rete: 172.24.0.0/16 anziché 192.168.0.0/16.

5. Poiché stiamo cambiando la rete dei servizi, per cui kubeadm emette certificati per l'apiserver (compreso), è necessario riemetterli:

  1. Controlliamo a quali domini e indirizzi IP è emesso l'attuale certificato:
    openssl x509 -noout -ext subjectAltName </etc/kubernetes/pki/apiserver.crt
    X509v3 Subject Alternative Name:
        DNS:dev-1-master, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:apiserver, IP Address:192.168.0.1, IP Address:10.0.0.163, IP Address:192.168.199.100
  2. Prepareremo una configurazione minima per kubeadm:
    cat kubeadm-config.yaml
    apiVersion: kubeadm.k8s.io/v1beta1
    kind: ClusterConfiguration
    networking:
      podSubnet: "10.244.0.0/16"
      serviceSubnet: "172.24.0.0/16"
    apiServer:
      certSANs:
      - "192.168.199.100" # Indirizzo IP del nodo master
  3. Eliminiamo i vecchi crt e key, poiché senza questo il nuovo certificato non verrà emesso:
    rm /etc/kubernetes/pki/apiserver.{key,crt}
  4. Riemettiamo i certificati per l'API server:
    kubeadm init phase certs apiserver --config=kubeadm-config.yaml
  5. Verifichiamo che il certificato sia stato emesso per la nuova rete:
    openssl x509 -noout -ext subjectAltName </etc/kubernetes/pki/apiserver.crt
    X509v3 Subject Alternative Name:
        DNS:kube-2-master, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, IP Address:172.24.0.1, IP Address:10.0.0.163, IP Address:192.168.199.100
  6. Dopo la riemissione del certificato API server, riavviamo il suo container:
    docker ps | grep k8s_kube-apiserver | awk '{print $1}' | xargs docker restart
  7. Rigeneriamo la configurazione per admin.conf:
    kubeadm alpha certs renew admin.conf
  8. Modifichiamo i dati in etcd:
    ./etcdhelper -cacert /etc/kubernetes/pki/etcd/ca.crt -cert /etc/kubernetes/pki/etcd/server.crt -key /etc/kubernetes/pki/etcd/server.key -endpoint https://127.0.0.1:2379 change-service-cidr 172.24.0.0/16 

    Attenzione! In questo momento, nel cluster smette di funzionare la risoluzione dei nomi, poiché nei pod esistenti è /etc/resolv.conf registrato il vecchio indirizzo di CoreDNS (kube-dns), mentre kube-proxy ha modificato le regole iptables dalla vecchia sottorete a quella nuova. L'articolo discute ora delle possibili opzioni per minimizzare i tempi di inattività.

  9. Modifichiamo i ConfigMap nello spazio dei nomi kube-system:
    kubectl -n kube-system edit cm kubelet-config-1.16

    — qui sostituiamo clusterDNS con il nuovo indirizzo IP del servizio kube-dns: kubectl -n kube-system get svc kube-dns.

    kubectl -n kube-system edit cm kubeadm-config

    — modifichiamo data.ClusterConfiguration.networking.serviceSubnet in una nuova rete.

  10. Poiché è cambiato l'indirizzo di kube-dns, è necessario aggiornare la configurazione di kubelet su tutti i nodi:
    kubeadm upgrade node phase kubelet-config && systemctl restart kubelet
  11. Rimane da riavviare tutti i pod nel cluster:
    kubectl get pods --no-headers=true --all-namespaces |sed -r 's\/(S+)s+(S+).*\/kubectl --namespace 1 delete pod 2\/e'

Minimizzazione dei tempi di inattività

Riflessioni su come minimizzare i downtime:

  1. Dopo le modifiche ai manifesti del control plane, creare un nuovo servizio kube-dns, ad esempio, chiamato kube-dns-tmp e con un nuovo indirizzo 172.24.0.10.
  2. Creare if in etcdhelper, che non modificherà il servizio kube-dns.
  3. Sostituire in tutti i kubelet l'indirizzo ClusterDNS con il nuovo, mentre il vecchio servizio continuerà a funzionare contemporaneamente al nuovo.
  4. Aspettare che i pod con le applicazioni si aggiorni o per motivi naturali o in un momento concordato.
  5. Eliminare il servizio kube-dns-tmp e cambiare serviceSubnetCIDR per il servizio kube-dns.

Questo piano permetterà di ridurre al minimo il downtime a ~un minuto — durante l'eliminazione del servizio kube-dns-tmp e la sostituzione della sottorete per il servizio dnsmasq.

Modifica di podNetwork

Nel frattempo, abbiamo deciso di vedere come modificare podNetwork utilizzando il nuovo etcdhelper. L'ordine delle operazioni è il seguente:

  • modificare le configurazioni in kube-system;
  • modificare il manifesto di kube-controller-manager;
  • cambiare podCIDR direttamente in etcd;
  • riavviare tutti i nodi del cluster.

Ora più in dettaglio su queste operazioni:

1. Modifichiamo i ConfigMap nello spazio dei nomi kube-system:

kubectl -n kube-system edit cm kubeadm-config

— modifichiamo data.ClusterConfiguration.networking.podSubnet con la nuova sottorete 10.55.0.0/16.

kubectl -n kube-system edit cm kube-proxy

— modifichiamo data.config.conf.clusterCIDR: 10.55.0.0/16.

2. Modifichiamo il manifesto di controller-manager:

vim /etc/kubernetes/manifests/kube-controller-manager.yaml

— modifichiamo --cluster-cidr=10.55.0.0/16.

3. Controlliamo i valori attuali .spec.podCIDR, .spec.podCIDRs, .InternalIP, .status.addresses per tutti i nodi del cluster:

kubectl get no -o json | jq '[.items[] | {"name": .metadata.name, "podCIDR": .spec.podCIDR, "podCIDRs": .spec.podCIDRs, "InternalIP": (.status.addresses[] | select(.type == "InternalIP") | .address)}]'

[
  {
    "name": "kube-2-master",
    "podCIDR": "10.244.0.0/24",
    "podCIDRs": [
      "10.244.0.0/24"
    ],
    "InternalIP": "192.168.199.2"
  },
  {
    "name": "kube-2-master",
    "podCIDR": "10.244.0.0/24",
    "podCIDRs": [
      "10.244.0.0/24"
    ],
    "InternalIP": "10.0.1.239"
  },
  {
    "name": "kube-2-worker-01f438cf-579f9fd987-5l657",
    "podCIDR": "10.244.1.0/24",
    "podCIDRs": [
      "10.244.1.0/24"
    ],
    "InternalIP": "192.168.199.222"
  },
  {
    "name": "kube-2-worker-01f438cf-579f9fd987-5l657",
    "podCIDR": "10.244.1.0/24",
    "podCIDRs": [
      "10.244.1.0/24"
    ],
    "InternalIP": "10.0.4.73"
  }
]

4. Sostituiremo podCIDR, apportando modifiche direttamente in etcd:

./etcdhelper -cacert /etc/kubernetes/pki/etcd/ca.crt -cert /etc/kubernetes/pki/etcd/server.crt -key /etc/kubernetes/pki/etcd/server.key -endpoint https://127.0.0.1:2379 change-pod-cidr 10.55.0.0/16

5. Controlliamo che il podCIDR sia effettivamente cambiato:

kubectl get no -o json | jq '[.items[] | {"name": .metadata.name, "podCIDR": .spec.podCIDR, "podCIDRs": .spec.podCIDRs, "InternalIP": (.status.addresses[] | select(.type == "InternalIP") | .address)}]'

[
  {
    "name": "kube-2-master",
    "podCIDR": "10.55.0.0/24",
    "podCIDRs": [
      "10.55.0.0/24"
    ],
    "InternalIP": "192.168.199.2"
  },
  {
    "name": "kube-2-master",
    "podCIDR": "10.55.0.0/24",
    "podCIDRs": [
      "10.55.0.0/24"
    ],
    "InternalIP": "10.0.1.239"
  },
  {
    "name": "kube-2-worker-01f438cf-579f9fd987-5l657",
    "podCIDR": "10.55.1.0/24",
    "podCIDRs": [
      "10.55.1.0/24"
    ],
    "InternalIP": "192.168.199.222"
  },
  {
    "name": "kube-2-worker-01f438cf-579f9fd987-5l657",
    "podCIDR": "10.55.1.0/24",
    "podCIDRs": [
      "10.55.1.0/24"
    ],
    "InternalIP": "10.0.4.73"
  }
]

6. Riavvieremo uno dopo l'altro tutti i nodi del cluster.

7. Se anche solo un nodo rimane con il vecchio podCIDR, kube-controller-manager non riuscirà a partire e i pod nel cluster non verranno pianificati.

In realtà, è possibile modificare il podCIDR anche in modo più semplice (ad esempio, così). Ma volevamo imparare a lavorare direttamente con etcd, perché ci sono casi in cui modificare gli oggetti Kubernetes in etcd è l'unica opzione possibile. (Ad esempio, non si può cambiare semplicemente il campo di Service spec.clusterIP.)

Risultato

L'articolo esamina la possibilità di lavorare con i dati in etcd direttamente, cioè bypassando l'API Kubernetes. A volte questo approccio consente di fare "trucchi". Le operazioni descritte nel testo sono state testate su reali cluster K8s. Tuttavia, il loro stato di prontezza per un uso generalizzato è PoC (proof of concept). Pertanto, se desideri utilizzare una versione modificata dello strumento etcdhelper sui tuoi cluster, fallo a tuo rischio e pericolo.

P.S.

Leggi 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