Experiența noastră cu datele din clusterul etcd Kubernetes direct (fără API K8s)

Din ce în ce mai des, clienții ne cer să asigurăm accesul la un cluster Kubernetes pentru a putea accesa serviciile din interiorul cluster-ului: astfel încât să se poată conecta direct la o bază de date sau la un serviciu, pentru a lega aplicația locală de aplicațiile din cluster…

Experiența noastră cu datele din clusterul etcd Kubernetes direct (fără API K8s)

De exemplu, apare necesitatea de a te conecta de pe mașina ta locală la serviciul memcached.staging.svc.cluster.local. Oferim această posibilitate prin VPN-ul din interiorul cluster-ului, la care se conectează clientul. Pentru acest lucru, anunțăm subrețelele pod-urilor, serviciilor și împingem DNS-ul cluster-ului către client. Astfel, atunci când clientul încearcă să se conecteze la serviciul memcached.staging.svc.cluster.local, cererea ajunge la DNS-ul cluster-ului și în răspuns primește adresa acestui serviciu din rețeaua de servicii a cluster-ului sau adresa pod-ului.

Configurăm clusterele K8s folosind kubeadm, unde, în mod implicit, subrețeaua de servicii este 192.168.0.0/16, iar rețeaua pod-urilor este 10.244.0.0/16. De obicei, totul funcționează bine, dar există câteva aspecte:

  • Subrețeaua 192.168.*.* este utilizată frecvent în rețelele de birou ale clienților, dar și mai des în rețelele de acasă ale dezvoltatorilor. Și atunci avem conflicte: routerele de acasă funcționează în această subrețea și VPN-ul împinge aceste subrețele din cluster către client.
  • Avem mai multe clustere (clustere production, stage și/sau mai multe clustere dev). Așadar, în toate acestea, subrețelele pentru pod-uri și servicii vor fi identice în mod implicit, ceea ce creează mari dificultăți pentru lucrul simultan cu serviciile în mai multe clustere.

Am adoptat deja de ceva timp practica de a folosi subrețele diferite pentru servicii și pod-uri în cadrul unui singur proiect — în general, pentru ca toate clusterele să aibă rețele diferite. Totuși, există un număr mare de clustere în funcțiune pe care nu vrem să le refacem de la zero, deoarece în ele rulează multe servicii, aplicații stateful etc.

Și atunci ne-am pus întrebarea: cum am putea schimba subrețeaua în cluster-ul existent?

Căutarea soluțiilor

Practicile cele mai comune implică recrearea tot serviciilor de tip ClusterIP. Ca variantă, pot recomanda și așa:

Următorul proces are o problemă: după ce totul este configurat, pod-urile vin cu vechiul IP ca server DNS în /etc/resolv.conf.
Deoarece încă nu am găsit soluția, a trebuit să resetez întregul cluster cu kubeadm reset și să-l inițializez din nou.

Dar nu tuturor li se potrivește… Iată informațiile mai detaliate pentru cazul nostru:

  • Se folosește Flannel;
  • Există clustere atât în cloud, cât și pe hardware;
  • Dorim să evităm reimplementarea tuturor serviciilor din cluster;
  • Există o necesitate de a face totul cu un minim de probleme;
  • Versiunea Kubernetes este 1.16.6 (totuși, acțiunile ulterioare vor fi similare și pentru alte versiuni);
  • Principala sarcină constă în a înlocui în cluster-ul desfășurat cu kubeadm subrețeaua de servicii 192.168.0.0/16, schimbând-o în 172.24.0.0/16.

Și s-a întâmplat că am fost de mult timp curios să vedem ce și cum se stochează în Kubernetes în etcd, ce putem face cu acest lucru... Așa că ne-am gândit: „Ce-ar fi să actualizăm pur și simplu datele în etcd, înlocuind vechile adrese IP (subrețeaua) cu cele noi??»

Căutând instrumente gata făcute pentru a lucra cu datele în etcd, nu am găsit nimic care să rezolve complet sarcina stabilită. (Apropo, dacă știți despre orice utilitare pentru a lucra direct cu datele în etcd - am fi recunoscători pentru linkuri.) Cu toate acestea, un punct de plecare bun a fost etcdhelper de la OpenShift (mulțumim autorilor săi!).

Această utilitate poate să se conecteze la etcd folosind certificate și să citească datele de acolo folosind comenzi ls, get, dump.

Adăugăm etcdhelper

Următoarea idee este evidentă: „Ce ne împiedică să extindem această utilitate, adăugând posibilitatea de a scrie date în etcd?”

Aceasta s-a concretizat într-o versiune modificată a etcdhelper cu două funcții noi changeServiceCIDR și changePodCIDR. Puteți vizualiza codul ei aici.

Ce fac noile funcții? Algoritmul changeServiceCIDR:

  • crează un deserializator;
  • compilează o expresie regulată pentru a înlocui CIDR;
  • parcurge toate serviciile de tip ClusterIP din cluster:
    • decodează valoarea din etcd într-un obiect Go;
    • folosind expresia regulată, înlocuiește primele două octeți ai adresei;
    • asignează serviciului o adresă IP din noua subrețea;
    • creează un serializator, transformă obiectul Go în protobuf, scrie datele noi în etcd.

Funcția changePodCIDR este, în esență, asemănătoare changeServiceCIDR — doar că, în loc să edităm specificațiile serviciilor, facem asta pentru nod și schimbăm .spec.PodCIDR în noua subrețea.

Practică

Schimbarea serviceCIDR

Planul de implementare al sarcinii stabilite este foarte simplu, dar implică downtime în momentul recreateării tuturor pod-urilor din cluster. După descrierea pașilor principali, ne vom împărtăși și gândurile despre cum, în teorie, s-ar putea minimiza această perioadă de nefuncționare.

Acțiuni pregătitoare:

  • instalarea software-ului necesar și compilarea etcdhelper-ului patch-uite;
  • backup etcd și /etc/kubernetes.

Un plan succint de acțiune pentru schimbarea serviceCIDR:

  • modificarea manifestelor apiserver-ului și controller-manager-ului;
  • reemiterea certificatelor;
  • modificarea serviciilor ClusterIP în etcd;
  • repornirea tuturor pod-urilor din cluster.

Iată secvența completă a acțiunilor în detaliu.

1. Instalăm etcd-client pentru a salva datele:

apt install etcd-client

2. Construim etcdhelper:

  • Instalăm 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
  • Ne salvăm etcdhelper.go, descărcăm dependențele, construim:
    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. Facem backup pentru 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. Schimbăm subrețeaua serviciilor în manifestele Kubernetes control plane. În fișiere /etc/kubernetes/manifests/kube-apiserver.yaml și /etc/kubernetes/manifests/kube-controller-manager.yaml modificăm parametrul --service-cluster-ip-range la noua subrețea: 172.24.0.0/16 în loc de 192.168.0.0/16.

5. Deoarece schimbăm subrețeaua serviciilor pentru care kubeadm emite certificate pentru apiserver (inclusiv), trebuie să le reemitim:

  1. Să vedem pentru ce domenii și adrese IP a fost emis certificatul curent:
    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. Să pregătim un config minim pentru 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" # adresa IP a nodului master
  3. Vom șterge vechile crt și key, deoarece fără acestea noul certificat nu va fi emis:
    rm /etc/kubernetes/pki/apiserver.{key,crt}
  4. Reemitem certificatele pentru API-server:
    kubeadm init phase certs apiserver --config=kubeadm-config.yaml
  5. Verificăm că certificatul a fost emis pentru noua subrețea:
    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. După reemiterea certificatului API-server, vom reporni containerul său:
    docker ps | grep k8s_kube-apiserver | awk '{print $1}' | xargs docker restart
  7. Regenerează config pentru admin.conf:
    kubeadm alpha certs renew admin.conf
  8. Vom edita datele în 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 

    Atenție! În acest moment, rezolvarea domeniilor din cluster se oprește, deoarece în pod-urile deja existente se /etc/resolv.conf folosește adresa veche CoreDNS (kube-dns), iar kube-proxy a modificat regulile iptables de la subrețeaua veche la cea nouă. În continuare, articolul discută despre posibile variante de a minimiza timpul de nefuncționare.

  9. Să corectăm ConfigMap-urile în spațiul de nume kube-system:
    kubectl -n kube-system edit cm kubelet-config-1.16

    — aici vom înlocui clusterDNS cu noua adresă IP a serviciului kube-dns: kubectl -n kube-system get svc kube-dns.

    kubectl -n kube-system edit cm kubeadm-config

    — vom corecta data.ClusterConfiguration.networking.serviceSubnet în noua subrețea.

  10. Având în vedere că adresa kube-dns s-a schimbat, este necesar să actualizăm configurația kubelet pe toate nodurile:
    kubeadm upgrade node phase kubelet-config && systemctl restart kubelet
  11. Rămâne să repornim toate pod-urile din cluster:
    kubectl get pods --no-headers=true --all-namespaces | sed -r 's\/(S+)s+(S+).*\/kubectl --namespace 1 delete pod 2\/e'

Minimizarea timpului de nefuncționare

Gânduri despre cum putem minimiza downtime-ul:

  1. După modificările manifestelor control plane-ului, creați un nou serviciu kube-dns, de exemplu, numit kube-dns-tmp cu o nouă adresă 172.24.0.10.
  2. Să facem if în etcdhelper, care nu va modifica serviciul kube-dns.
  3. Să înlocuim în toate kubelet-urile adresa ClusterDNS cu noua, în timp ce serviciul vechi va continua să funcționeze simultan cu noul.
  4. Așteptăm ca pod-urile cu aplicații să se reîntoarcă fie prin motive naturale, fie la o oră convenită.
  5. Eliminăm serviciul kube-dns-tmp și schimbăm serviceSubnetCIDR pentru serviciul kube-dns.

Acest plan va permite minimizarea downtime-ului la ~minute — pentru timpul necesar eliminării serviciului kube-dns-tmp și schimbării subrețelei pentru serviciu. Cea mai interesantă și nouă pentru mine a fost cauza semnificativă a creșterii traficului de cereri DNS. Despre aceasta, dar și despre ce se poate face, este postarea mea..

Modificarea podNetwork

De asemenea, am decis să vedem cum putem modifica podNetwork folosind etcdhelper-ul obținut. Secvența acțiunilor este următoarea:

  • corectăm configurațiile în kube-system;
  • corectăm manifestul kube-controller-manager-ului;
  • modificăm podCIDR direct în etcd;
  • rebootăm toate nodurile din cluster.

Acum, mai în detaliu despre aceste acțiuni:

1. Modificăm ConfigMap-urile în spațiul de nume kube-system:

kubectl -n kube-system edit cm kubeadm-config

— corectăm data.ClusterConfiguration.networking.podSubnet cu noua subrețea 10.55.0.0/16.

kubectl -n kube-system edit cm kube-proxy

— corectăm data.config.conf.clusterCIDR: 10.55.0.0/16.

2. Modificăm manifestul controller-manager-ului:

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

— corectăm --cluster-cidr=10.55.0.0/16.

3. Verificăm valorile curente .spec.podCIDR, .spec.podCIDRs, .InternalIP, .status.addresses pentru toate nodurile din 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. Vom schimba podCIDR, făcând modificări direct în 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. Vom verifica dacă podCIDR s-a schimbat cu adevărat:

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. Vom reporni fiecare nod din cluster pe rând.

7. Dacă măcar un nod păstrează vechiul podCIDR, atunci kube-controller-manager nu va putea porni, iar podurile din cluster nu vor fi planificate.

De fapt, schimbarea podCIDR-ului poate fi realizată și mai simplu (de exemplu, . Cu ajutorul acestuia, se pot instala un cluster Kafka pe Kubernetes cu doar câteva comenzi:). Dar am dorit să învățăm să lucrăm direct cu etcd, deoarece există cazuri în care modificarea obiectelor Kubernetes în etcd este singura opțiune posibilă. (De exemplu, nu poți pur și simplu să schimbi câmpul de la Service. spec.clusterIP.)

Rezultatul

Articolul prezintă posibilitatea de a lucra cu date în etcd direct, adică fără a folosi API-ul Kubernetes. Uneori, o astfel de abordare permite realizarea de „foci inteligente”. Operațiile prezentate în text au fost testate pe clustere K8s reale. Totuși, statutul lor privind utilizarea pe scară largă este - PoC (proof of concept). Prin urmare, dacă doriți să utilizați o versiune modificată a utilitarului etcdhelper pe clusterele dumneavoastră, faceți-o pe propriul risc.

P.S.

Citiți și în blogul nostru:

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster