Unsere Erfahrung mit Daten in einem etcd Kubernetes-Cluster direkt (ohne K8s API)

Immer hĂ€ufiger wenden sich Kunden an uns mit der Bitte, Zugang zum Kubernetes-Cluster zu ermöglichen, um auf Dienste innerhalb des Clusters zugreifen zu können: um sich direkt mit einer Datenbank oder einem Dienst zu verbinden und um eine lokale Anwendung mit Anwendungen innerhalb des Clusters zu verbinden


Unsere Erfahrung mit Daten in einem etcd Kubernetes-Cluster direkt (ohne K8s API)

Zum Beispiel besteht die Notwendigkeit, sich von seinem lokalen Computer aus mit dem Dienst memcached.staging.svc.cluster.localzu verbinden. Wir bieten diese Möglichkeit ĂŒber ein VPN innerhalb des Clusters an, zu dem der Kunde eine Verbindung herstellt. Dazu kĂŒndigen wir die Subnetze der Pods, Dienste an und pushen die Cluster-DNS an den Kunden. Somit, wenn der Kunde versucht, sich mit dem Dienst memcached.staging.svc.cluster.local, wird die Anfrage an das DNS des Clusters gesendet und erhĂ€lt als Antwort die Adresse dieses Dienstes aus dem Dienstnetz des Clusters oder die Adresse des Pods.

K8s-Cluster konfigurieren wir mit kubeadm, wobei das Dienst-Subnetz standardmĂ€ĂŸig 192.168.0.0/16ist, und das Pod-Subnetz 10.244.0.0/16ist. Normalerweise funktioniert alles gut, aber es gibt ein paar Punkte:

  • Das Subnetz 192.168.*.* wird oft in den BĂŒronetzwerken der Kunden verwendet, und noch hĂ€ufiger in den Netzwerken von Entwicklern zu Hause. Dann gibt es Konflikte: Heim-Router arbeiten in diesem Subnetz und das VPN pusht diese Subnetze aus dem Cluster an den Kunden.
  • Wir haben mehrere Cluster (Produktions-, Stage- und/oder mehrere Dev-Cluster). Dann haben alle von ihnen standardmĂ€ĂŸig dieselben Subnetze fĂŒr Pods und Dienste, was erhebliche Schwierigkeiten bei der gleichzeitigen Nutzung von Diensten in mehreren Clustern verursacht.

Wir haben vor einiger Zeit die Praxis eingefĂŒhrt, verschiedene Subnetze fĂŒr Dienste und Pods im Rahmen eines Projekts zu verwenden – um insgesamt sicherzustellen, dass alle Cluster unterschiedliche Netzwerke haben. Allerdings gibt es eine große Anzahl von Clustern, die in Betrieb sind und die wir nicht von Grund auf neu aufsetzen möchten, da dort viele Dienste, Stateful-Anwendungen usw. laufen.

Und dann haben wir uns gefragt: Wie könnte man das Subnetz in einem bestehenden Cluster Àndern?

Die Suche nach Lösungen

Die gÀngigste Praxis besteht darin, die Ressourcen) abgeschlossen ist, lösen wir Dienste vom Typ ClusterIP neu zu erstellen. Als Alternative können sie empfehlen und so weiter: Der folgende Prozess hat ein Problem: Nach der gesamten Konfiguration kommen die Pods mit der alten IP als DNS-Namensserver in /etc/resolv.conf.

Da ich die Lösung immer noch nicht gefunden habe, musste ich den gesamten Cluster mit kubeadm zurĂŒcksetzen und ihn erneut initialisieren.
Aber das passt nicht fĂŒr jeden
 Hier sind detailliertere Informationen fĂŒr unseren Fall:

Es wird Flannel verwendet;

  • Flannel wird verwendet;
  • Es gibt Cluster sowohl in der Cloud als auch auf Hardware;
  • Wir wĂŒrden gerne vermeiden, alle Dienste im Cluster erneut bereitzustellen;
  • Es besteht das BedĂŒrfnis, alles mit minimalen Problemen zu erledigen;
  • Die Kubernetes-Version ist 1.16.6 (die weiteren Schritte sind jedoch auch fĂŒr andere Versionen analog);
  • Die Hauptaufgabe besteht darin, im Cluster, der mit kubeadm mit einem Dienst-Subnetz bereitgestellt wurde, 192.168.0.0/16, es durch zu ersetzen 172.24.0.0/16.

Und es kam gerade passend, dass wir schon lange interessiert waren zu sehen, was und wie in Kubernetes in etcd gespeichert ist, was man damit ĂŒberhaupt machen kann... Also dachten wir: "Warum einfach nicht die Daten in etcd aktualisieren, indem wir die alten IP-Adressen (Subnetz) durch neue ersetzen?»

Auf der Suche nach fertigen Werkzeugen zur Arbeit mit den Daten in etcd fanden wir nichts, das das gestellte Ziel vollstĂ€ndig löst. (Übrigens, wenn Sie von irgendwelchen Tools wissen, um direkt mit Daten in etcd zu arbeiten – wir wĂ€ren dankbar fĂŒr Links.) Eine gute Ausgangsbasis war jedoch etcdhelper von OpenShift (danke an die Autoren!).

Dieses Tool kann sich mithilfe von Zertifikaten mit etcd verbinden und Daten ĂŒber Befehle lesen ls, get, dump.

Wir erweitern etcdhelper

Der nÀchste Gedanke ist naheliegend: "Was hindert uns daran, diese Utility zu erweitern, um die Möglichkeit zu schaffen, Daten in etcd zu schreiben?"

Dies wurde in einer modifizierten Version von etcdhelper mit zwei neuen Funktionen umgesetzt changeServiceCIDR und changePodCIDR. Den Code kann man einsehen hier.

Was machen die neuen Funktionen? Algorithmus changeServiceCIDR:

  • wir erstellen einen Deserialisierer;
  • wir kompilieren einen regulĂ€ren Ausdruck zur Ersetzung des CIDR;
  • wir durchlaufen alle Dienste vom Typ ClusterIP im Cluster:
    • wir dekodieren den Wert aus etcd in ein Go-Objekt;
    • wir ersetzen die ersten beiden Bytes der Adresse mithilfe eines regulĂ€ren Ausdrucks;
    • wir weisen dem Dienst eine IP-Adresse aus dem neuen Subnetz zu;
    • wir erstellen einen Serialisierer, wandeln das Go-Objekt in protobuf um, und schreiben die neuen Daten in etcd.

Funktion changePodCIDR ist im Wesentlichen analog changeServiceCIDR — nur anstelle der Bearbeitung der Spezifikation der Dienste machen wir dies fĂŒr den Knoten und Ă€ndern .spec.PodCIDR in das neue Subnetz.

Praxis

Änderung des serviceCIDR

Der Plan zur Umsetzung der gestellten Aufgabe ist sehr einfach, bedeutet jedoch Downtime wÀhrend der Wiederherstellung aller Pods im Cluster. Nach der Beschreibung der wichtigsten Schritte werden wir auch Gedanken dazu teilen, wie man diese Ausfallzeit theoretisch minimieren kann.

Vorbereitende Maßnahmen:

  • Installation der erforderlichen Software und Zusammenstellung des gepatchten etcdhelper;
  • Backup von etcd und /etc/kubernetes.

Kurzer Aktionsplan zur Änderung von serviceCIDR:

  • Änderung der Manifeste des apiserver und des controller-managers;
  • Zertifikate erneut ausstellen;
  • Änderung der ClusterIP-Dienste in etcd;
  • Neustart aller Pods im Cluster.

Im Folgenden finden Sie die vollstĂ€ndige Schritt-fĂŒr-Schritt-Anleitung.

1. Installieren Sie den etcd-Client zum Daten-Dump:

apt install etcd-client

2. Erstellen Sie etcdhelper:

  • Installieren Sie 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
  • Speichern wir uns etcdhelper.go, laden wir die AbhĂ€ngigkeiten herunter und bauen:
    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. Machen Sie ein Backup von 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. Ändern Sie das Service-Subnetz in den Manifesten des Kubernetes-Control-Plans. In den Dateien /etc/kubernetes/manifests/kube-apiserver.yaml und /etc/kubernetes/manifests/kube-controller-manager.yaml Ă€ndern wir den Parameter --service-cluster-ip-range in das neue Subnetz: 172.24.0.0/16 anstatt 192.168.0.0/16.

5. Da wir das Service-Subnetz Ă€ndern, fĂŒr das kubeadm die Zertifikate fĂŒr den apiserver (unter anderem) ausstellt, mĂŒssen diese erneut ausgestellt werden:

  1. Lassen Sie uns ansehen, fĂŒr welche Domains und IP-Adressen das aktuelle Zertifikat ausgestellt wurde:
    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. Bereiten wir eine minimale Konfiguration fĂŒr kubeadm vor:
    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" # IP-Adresse des Master-Knotens
  3. Löschen Sie die alten crt und key, da ohne dies das neue Zertifikat nicht ausgestellt wird:
    rm \/etc\/kubernetes\/pki\/apiserver.{key,crt}
  4. Stellen Sie die Zertifikate fĂŒr den API-Server erneut aus:
    kubeadm init phase certs apiserver --config=kubeadm-config.yaml
  5. ÜberprĂŒfen Sie, dass das Zertifikat fĂŒr das neue Subnetz ausgestellt wurde:
    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. Nach der erneuten Ausstellung des Zertifikats fĂŒr den API-Server starten wir seinen Container neu:
    docker ps | grep k8s_kube-apiserver | awk '{print $1}' | xargs docker restart
  7. Regenerieren Sie die Konfiguration fĂŒr admin.conf:
    kubeadm alpha certs renew admin.conf
  8. Bearbeiten Sie die Daten 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 

    Achtung! In diesem Moment funktioniert die Auflösung von DomÀnen im Cluster nicht mehr, da in den bereits vorhandenen Pods die /etc/resolv.conf alte Adresse von CoreDNS (kube-dns) eingetragen ist und kube-proxy die iptables-Regeln von dem alten Subnetz auf das neue geÀndert hat. Im Folgenden wird in dem Artikel beschrieben, wie man die Ausfallzeiten minimieren kann.

  9. Lass uns die ConfigMaps im Namensraum kube-system:
    kubectl -n kube-system edit cm kubelet-config-1.16

    — hier ersetzen wir clusterDNS mit der neuen IP-Adresse des kube-dns-Dienstes: kubectl -n kube-system get svc kube-dns.

    kubectl -n kube-system edit cm kubeadm-config

    — wir korrigieren data.ClusterConfiguration.networking.serviceSubnet in das neue Subnetz.

  10. Da sich die Adresse von kube-dns geÀndert hat, ist es notwendig, die kubelet-Konfiguration auf allen Knoten zu aktualisieren:
    kubeadm upgrade node phase kubelet-config && systemctl restart kubelet
  11. Es bleibt nur noch, alle Pods im Cluster neu zu starten:
    kubectl get pods --no-headers=true --all-namespaces | sed -r 's/(S+)s+(S+).*/kubectl --namespace 1 delete pod 2/e'

Minimierung der Ausfallzeit

Überlegungen, wie man die Downtime minimieren kann:

  1. Nach den Änderungen der Manifeste des Control-Plans einen neuen kube-dns-Dienst erstellen, zum Beispiel mit dem Namen kube-dns-tmp und mit der neuen Adresse 172.24.0.10.
  2. Erstellen if in etcdhelper, der den kube-dns-Dienst nicht modifizieren wird.
  3. Die Adresse ClusterDNS in allen kubelets durch die neue ersetzen, wÀhrend der alte Dienst gleichzeitig mit dem neuen weiterarbeitet.
  4. Warten, bis die Pods mit den Anwendungen entweder von selbst aus natĂŒrlichen GrĂŒnden umgeschichtet werden oder zu einem vereinbarten Zeitpunkt.
  5. Den Dienst löschen kube-dns-tmp und die serviceSubnetCIDR fĂŒr den kube-dns-Dienst Ă€ndern.

Dieser Plan wird die Downtime auf ~eine Minute minimieren — wĂ€hrend der Zeit des Löschens des Dienstes kube-dns-tmp und des Wechsels des Subnetzes fĂŒr den Dienst kube-dns.

Modifikation des podNetwork

Wir haben auch beschlossen, zu prĂŒfen, wie das podNetwork mit dem erhaltenen etcdhelper modifiziert werden kann. Die Abfolge der Schritte sieht folgendermaßen aus:

  • wir korrigieren die Konfigurationen in kube-system;
  • wir korrigieren das Manifest des kube-controller-managers;
  • wir Ă€ndern podCIDR direkt in etcd;
  • wir starten alle Knoten im Cluster neu.

Nun im Detail zu diesen Aktionen:

1. Wir modifizieren die ConfigMaps im Namensraum kube-system:

kubectl -n kube-system edit cm kubeadm-config

— wir korrigieren data.ClusterConfiguration.networking.podSubnet auf das neue Subnetz 10.55.0.0/16.

kubectl -n kube-system edit cm kube-proxy

— wir korrigieren data.config.conf.clusterCIDR: 10.55.0.0/16.

2. Wir modifizieren das Manifest des controller-managers:

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

— wir korrigieren --cluster-cidr=10.55.0.0/16.

3. Wir betrachten die aktuellen Werte .spec.podCIDR, .spec.podCIDRs, .InternalIP, .status.addresses fĂŒr alle Knoten im 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. Wir ersetzen podCIDR, indem wir die Änderungen direkt in etcd vornehmen:

./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. Wir ĂŒberprĂŒfen, ob sich der podCIDR tatsĂ€chlich geĂ€ndert hat:

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. Wir starten nacheinander alle Knoten des Clusters neu.

7. Wenn bei mindestens einem Knoten der alte podCIDR verbleibt alt podCIDR, kann der kube-controller-manager nicht gestartet werden, und die Pods im Cluster werden nicht geplant.

TatsĂ€chlich kann die Änderung des podCIDR auch einfacher durchgefĂŒhrt werden (zum Beispiel, so). Aber wir wollten lernen, wie man direkt mit etcd arbeitet, da es Situationen gibt, in denen die Bearbeitung von Kubernetes-Objekten in etcd — die einzige mögliche Option ist. (Zum Beispiel kann das Feld spec.clusterIP bei einem Service nicht einfach ohne Ausfall geĂ€ndert werden..)

Fazit

In dem Artikel wird die Möglichkeit beschrieben, direkt mit den Daten in etcd zu arbeiten, d.h. um den Kubernetes-API zu umgehen. Manchmal erlaubt dieser Ansatz, „schlaue Dinge“ zu tun. Die im Text beschriebenen Operationen haben wir an realen K8s-Clustern getestet. Ihr Status bezĂŒglich der breiten Anwendbarkeit ist — PoC (Proof of Concept). Daher sollten Sie, wenn Sie eine modifizierte Version des etcdhelper-Tools in Ihren Clustern verwenden möchten, dies auf eigenes Risiko tun.

P.S.

Lesen Sie auch in unserem Blog:

Quelle: habr.com

60GB SSD 8Gb DDR4