Nasze doświadczenie z danymi w etcd klastra Kubernetes bezpośrednio (bez API K8s)

Coraz częściej klienci zwracają się do nas z prośbą o zapewnienie dostępu do klastra Kubernetes, aby móc korzystać z usług wewnątrz klastra: tak, aby można było bezpośrednio połączyć się z jakąś bazą danych lub usługą, dla połączenia lokalnej aplikacji z aplikacjami wewnątrz klastra…

Nasze doświadczenie z danymi w etcd klastra Kubernetes bezpośrednio (bez API K8s)

Na przykład, pojawia się potrzeba połączenia się z własnej lokalnej maszyny z usługą memcached.staging.svc.cluster.local. Oferujemy taką możliwość za pomocą VPN wewnątrz klastra, do którego łączy się klient. W tym celu ogłaszamy podsieci podów, usług i przesyłamy klientowi DNS klastra. W ten sposób, gdy klient próbuje połączyć się z usługą memcached.staging.svc.cluster.local, zapytanie trafia do DNS klastra, a w odpowiedzi otrzymuje adres danej usługi z sieci usług klastra lub adres poda.

Klastry K8s konfigurujemy za pomocą kubeadm, gdzie domyślnie sieć usługowa to 192.168.0.0/16, a sieć podów to 10.244.0.0/16. Zazwyczaj wszystko działa dobrze, ale są pewne kwestie:

  • Podsieć 192.168.*.* jest często używana w sieciach biurowych klientów, a jeszcze częściej — w domowych sieciach deweloperów. Wtedy pojawiają się konflikty: domowe routery działają w tej podsieci i VPN przesyła te podsieci z klastra do klienta.
  • Mamy kilka klastrów (klastry produkcyjne, przygotowawcze i/lub kilka klastrów deweloperskich). Zatem we wszystkich nich domyślnie będą takie same podsieci dla podów i usług, co stwarza dużą trudność w jednoczesnej pracy z usługami w kilku klastrach.

Już od dłuższego czasu przyjęliśmy praktykę używania różnych podsieci dla usług i podów w ramach jednego projektu — ogólnie, aby wszystkie klastry miały różne sieci. Jednak istnieje duża liczba klastrów w pracy, które nie chciałoby się przerabiać od podstaw, ponieważ uruchomionych jest w nich wiele usług, aplikacji z stanem itp.

I wtedy zadaliśmy sobie pytanie: jak zmienić podsieć w istniejącym klastrze?

Poszukiwanie rozwiązań

Najbardziej powszechną praktyką jest ponowne stworzenie wszystkie usług typu ClusterIP. Alternatywnie, mogą zasugerować i takie rozwiązanie:

Następujący proces ma problem: po skonfigurowaniu wszystkiego, pody uruchamiają się z starym IP jako serwerem nazw DNS w /etc/resolv.conf.
Ponieważ wciąż nie znalazłem rozwiązania, musiałem zresetować cały klaster za pomocą kubeadm reset i ponownie go zainicjować.

Ale nie wszystkim to odpowiada… Oto bardziej szczegółowe informacje dla naszego przypadku:

  • Używamy Flannela;
  • Są klastry zarówno w chmurze, jak i na sprzęcie;
  • Chciałbym uniknąć ponownego wdrażania wszystkich usług w klastrze;
  • Istnieje potrzeba, aby zrealizować wszystko przy minimalnej ilości problemów;
  • Wersja Kubernetes — 1.16.6 (zresztą dalsze działania będą analogiczne dla innych wersji);
  • Główne zadanie sprowadza się do tego, aby w klastrze, wdrożonym za pomocą kubeadm z podsiecią serwisową, 192.168.0.0/16, zastąpić ją na 172.24.0.0/16.

I tak się złożyło, że od dłuższego czasu interesowało nas, co i jak w Kubernetes jest przechowywane w etcd, co w ogóle można z tym zrobić… Więc pomyśleliśmy: „Dlaczego po prostu nie zaktualizować danych w etcd, zastępując stare adresy IP (podsiec) nowymi?

Szukając gotowych narzędzi do pracy z danymi w etcd, nie znaleźliśmy nic, co całkowicie rozwiązałoby postawione zadanie. (Przy okazji, jeśli znasz jakiekolwiek narzędzia do pracy z danymi bezpośrednio w etcd — będziemy wdzięczni za linki.) Jednak dobrą punktem wyjścia był etcdhelper od OpenShift (dzięki jego autorom!).

To narzędzie potrafi łączyć się z etcd za pomocą certyfikatów i odczytywać dane stamtąd za pomocą poleceń ls, get, dump.

Dopisywanie etcdhelper

Następna myśl jest logiczna: „Co powstrzymuje nas przed dopisaniem tej użyteczności, dodając możliwość zapisu danych w etcd?”

Zmaterializowała się w zmodyfikowanej wersji etcdhelper z dwiema nowymi funkcjami changeServiceCIDR i changePodCIDR. Jej kod można zobaczyć tutaj.

Co robią nowe funkcje? Algorytm changeServiceCIDR:

  • tworzymy deserializator;
  • kompilujemy wyrażenie regularne do zastąpienia CIDR;
  • przechodzimy przez wszystkie usługi typu ClusterIP w klastrze:
    • dekodujemy wartość z etcd w obiekt Go;
    • przy użyciu wyrażenia regularnego zastępujemy pierwsze dwa bajty adresu;
    • przypisujemy usłudze adres IP z nowej podsieci;
    • tworzymy serializator, przekształcamy obiekt Go na protobuf, zapisujemy nowe dane w etcd.

veth_xdp_flush_bq() changePodCIDR w zasadzie jest to analogiczne changeServiceCIDR — tylko zamiast edytowania specyfikacji usług robimy to dla węzła i zmieniamy .spec.PodCIDR na nową podsieć.

Praktyka

Zmiana serviceCIDR

Plan wdrożenia postawionego zadania jest bardzo prosty, ale wiąże się z przestojem w momencie ponownego tworzenia wszystkich podów w klastrze. Po opisaniu głównych kroków podzielimy się również pomysłami, jak teoretycznie można zminimalizować ten czas przestoju.

Działania przygotowawcze:

  • instalacja niezbędnego oprogramowania i kompilacja załatwionego etcdhelper;
  • backup etcd i /etc/kubernetes.

Krótki plan działań dotyczących zmiany serviceCIDR:

  • zmiana manifestów apiserver’a i controller-manager’a;
  • ponowna emisja certyfikatów;
  • zmiana ClusterIP usług w etcd;
  • restart wszystkich podów w klastrze.

Poniżej przedstawiono pełną sekwencję działań w szczegółach.

1. Instalujemy etcd-client do zrzutu danych:

apt install etcd-client

2. Budujemy etcdhelper:

  • Instalujemy 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
  • Zapisujemy sobie etcdhelper.go, pobieramy zależności, budujemy:
    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. Robimy kopię zapasową 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. Zmieniamy podsieć usługi w manifestach Kubernetes control plane. W plikach /etc/kubernetes/manifests/kube-apiserver.yaml i /etc/kubernetes/manifests/kube-controller-manager.yaml zmieniamy parametr --service-cluster-ip-range na nową podsieć: 172.24.0.0/16 zamiast 192.168.0.0/16.

5. Ponieważ zmieniamy podsieć usługi, dla której kubeadm wydaje certyfikaty dla apiserver’a (w tym), muszą zostać ponownie wydane:

  1. Sprawdźmy, na jakie domeny i adresy IP wydany jest aktualny certyfikat:
    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. Przygotujmy minimalny konfigurację dla 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" # Adres IP węzła master
  3. Usuniemy stare crt i key, ponieważ bez tego nowy certyfikat nie zostanie wydany:
    rm /etc/kubernetes/pki/apiserver.{key,crt}
  4. Ponownie wydamy certyfikaty dla API-serwera:
    kubeadm init phase certs apiserver --config=kubeadm-config.yaml
  5. Sprawdzimy, czy certyfikat został wydany dla nowej podsieci:
    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. Po ponownym wydaniu certyfikatu API-serwera uruchamiamy jego kontener ponownie:
    docker ps | grep k8s_kube-apiserver | awk '{print $1}' | xargs docker restart
  7. Ponownie generujemy konfigurację dla admin.conf:
    kubeadm alpha certs renew admin.conf
  8. Edytujemy dane w 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 

    Uwaga! W tym momencie w klastrze przestaje działać rozwiązywanie nazw domen, ponieważ w już istniejących podach zapisany jest stary adres CoreDNS (kube-dns), a kube-proxy zmienił reguły iptables z starej podsieci na nową. W dalszej części artykułu opisano możliwe sposób minimalizacji przestojów. /etc/resolv.conf Poprawimy ConfigMapy w przestrzeni nazw

  9. kubectl -n kube-system edit cm kubelet-config-1.16 kube-system:
    — tutaj zastąpimy

    clusterDNS nowym adresem IP usługi kube-dns: kubectl -n kube-system get svc kube-dns kubectl -n kube-system edit cm kubeadm-config.

    — poprawimy

    data.ClusterConfiguration.networking.serviceSubnet Ponieważ adres kube-dns uległ zmianie, konieczne jest zaktualizowanie konfiguracji kubeleta na wszystkich węzłach: na nową podsieć.

  10. kubeadm upgrade node phase kubelet-config && systemctl restart kubelet
    Pozostaje przemyśleć ponowne uruchomienie wszystkich podów w klastrze:
  11. kubectl get pods --no-headers=true --all-namespaces | sed -r 's/(S+)s+(S+).*/kubectl --namespace 1 delete pod 2/e'
    Minimalizacja przestojów

Rozważania, jak można zminimalizować czas przestoju:

Po wprowadzeniu zmian w manifestach control plane'a, stwórz nową usługę kube-dns, na przykład pod nazwą

  1. kube-dns-tmp i nowym adresem Utworzyć 172.24.0.10.
  2. w etcdhelper, który nie będzie modyfikować usługi kube-dns. if Zastąpić w wszystkich kubeletach adres
  3. ClusterDNS nowym, przy czym stara usługa będzie kontynuować działanie równolegle z nową. Poczekać, aż pody z aplikacjami zostaną zaktualizowane, albo same z naturalnych powodów, albo w ustalonym czasie.
  4. Usunąć usługę
  5. i zmienić i nowym adresem serviceSubnetCIDR dla usługi kube-dns. Ten plan pozwoli zminimalizować czas przestoju do około jednej minuty — w czasie usuwania usługi

i zmiany podsieci dla usługi i nowym adresem kube-dns Modyfikacja podNetwork.

Jednocześnie postanowiliśmy sprawdzić, jak zmodyfikować podNetwork z pomocą powstałego etcdhelpera. Kolejność działań wygląda następująco:

poprawiamy konfiguracje w

  • poprawiamy manifest kube-controller-manager’a; kube-system;
  • zmieniamy podCIDR bezpośrednio w etcd;
  • restartujemy wszystkie węzły klastra.
  • Teraz bardziej szczegółowo o tych działaniach:

1. Modyfikujemy ConfigMapy w przestrzeni nazw

— poprawiamy kube-system:

— poprawimy

data.ClusterConfiguration.networking.podSubnet na nową podsieć kubectl -n kube-system edit cm kube-proxy 10.55.0.0/16.

data.config.conf.clusterCIDR: 10.55.0.0/16

data.ClusterConfiguration.networking.podSubnet 2. Modyfikujemy manifest controller-managera:.

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

--cluster-cidr=10.55.0.0/16

data.ClusterConfiguration.networking.podSubnet 3. Sprawdzamy bieżące wartości.

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

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. Zmieniamy podCIDR, wprowadzając poprawki bezpośrednio w 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. Sprawdzamy, czy podCIDR rzeczywiście się zmienił:

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. Po kolei restartujemy wszystkie węzły klastra.

7. Jeśli chociaż jeden węzeł pozostawi stary podCIDR, to kube-controller-manager nie będzie mógł się uruchomić, a pod’y w klastrze nie będą planowane.

W rzeczywistości zmianę podCIDR można przeprowadzić w prostszy sposób (na przykład, tak). Ale chcieliśmy nauczyć się pracować z etcd bezpośrednio, ponieważ są przypadki, gdy edytowanie obiektów Kubernetes w etcd — jedyny możliwy wariant. (Na przykład, nie można po prostu tak bez przestoju zmienić w polu Service spec.clusterIP.)

Podsumowanie

W artykule omówiono możliwość pracy z danymi w etcd bezpośrednio, tzn. z pominięciem API Kubernetes. Czasami takie podejście pozwala na robienie „sprytnych rzeczy”. Operacje opisane w tekście testowaliśmy na rzeczywistych klastrach K8s. Jednak ich status gotowości do szerokiego zastosowania — PoC (proof of concept). Dlatego, jeśli chcesz używać zmodyfikowanej wersji narzędzia etcdhelper na swoich klastrach, rób to na własne ryzyko.

P.S.

Przeczytaj także na naszym blogu:

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster