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…

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, 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ą na172.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ł (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ć .
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-client2. 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:
- 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 - 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 - Usuniemy stare crt i key, ponieważ bez tego nowy certyfikat nie zostanie wydany:
rm /etc/kubernetes/pki/apiserver.{key,crt} - Ponownie wydamy certyfikaty dla API-serwera:
kubeadm init phase certs apiserver --config=kubeadm-config.yaml - 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 - Po ponownym wydaniu certyfikatu API-serwera uruchamiamy jego kontener ponownie:
docker ps | grep k8s_kube-apiserver | awk '{print $1}' | xargs docker restart - Ponownie generujemy konfigurację dla
admin.conf:kubeadm alpha certs renew admin.conf - 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/16Uwaga! 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.confPoprawimy ConfigMapy w przestrzeni nazw - kubectl -n kube-system edit cm kubelet-config-1.16
kube-system:— tutaj zastąpimyclusterDNS
nowym adresem IP usługi kube-dns:kubectl -n kube-system get svc kube-dnskubectl -n kube-system edit cm kubeadm-config.— poprawimydata.ClusterConfiguration.networking.serviceSubnet
Ponieważ adres kube-dns uległ zmianie, konieczne jest zaktualizowanie konfiguracji kubeleta na wszystkich węzłach:na nową podsieć. - kubeadm upgrade node phase kubelet-config && systemctl restart kubelet
Pozostaje przemyśleć ponowne uruchomienie wszystkich podów w klastrze: - 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ą
- kube-dns-tmp
i nowym adresemUtworzyć172.24.0.10. - w etcdhelper, który nie będzie modyfikować usługi kube-dns.
ifZastąpić w wszystkich kubeletach adres - 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. - Usunąć usługę
- i zmienić
i nowym adresemserviceSubnetCIDRdla 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/165. 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, ). 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
