Наш опыт работы с данными в etcd Kubernetes-кластера напрямую (без K8s API)
Все чаще к нам обращаются клиенты с просьбой обеспечить доступ в Kubernetes-кластер для возможности обращения к сервисам внутри кластера: чтобы можно было напрямую подключиться к какой-то базе данных или сервису, для связи локального приложения с приложениями внутри кластера…
Например, возникает потребность подключиться со своей локальной машины к сервису memcached.staging.svc.cluster.local. Мы предоставляем такую возможность с помощью VPN внутри кластера, к которому подключается клиент. Для этого анонсируем подсети pod’ов, сервисов и push’им кластерные DNS клиенту. Таким образом, когда клиент пытается подключиться к сервису memcached.staging.svc.cluster.local, запрос уходит в DNS кластера и в ответ получает адрес данного сервиса из сервисной сети кластера или адрес pod’а.
K8s-кластеры мы настраиваем с помощью kubeadm, где по умолчанию сервисная подсеть — 192.168.0.0/16, а сеть pod’ов — 10.244.0.0/16. Обычно всё хорошо работает, но есть пара моментов:
Подсеть 192.168.*.* часто используется в офисных сетях клиентов, а еще чаще — в домашних сетях разработчиков. И тогда у нас получаются конфликты: домашние роутеры работают в этой подсети и VPN push’ит эти подсети из кластера клиенту.
У нас есть несколько кластеров (кластеры production, stage и/или несколько dev-кластеров). Тогда во всех них по умолчанию будут одинаковые подсети для pod’ов и сервисов, что создает большие сложности для одновременной работы с сервисами в нескольких кластерах.
Мы уже довольно давно приняли практику использования различных подсетей для сервисов и pod’ов в рамках одного проекта — в общем, чтобы все кластеры были с разными сетями. Однако есть большое количество кластеров в работе, которые не хотелось бы перекатывать с нуля, так как в них запущены многие сервисы, stateful-приложения и т.п.
И тогда мы задались вопросом: как бы поменять подсеть в существующем кластере?
Поиск решений
Наиболее распространенная практика — пересоздать все сервисы с типом ClusterIP. Как вариант, могут посоветовать и такое:
The following process has a problem: after everything configured, the pods come up with the old IP as a DNS nameserver in /etc/resolv.conf.
Since I still did not find the solution, i had to reset the entire cluster with kubeadm reset and init it again.
Но не всем это подходит… Вот более детальные вводные для нашего случая:
Используется Flannel;
Есть кластера как в облаках, так и на железе;
Хотелось бы избежать повторного деплоя всех сервисов в кластере;
Есть потребность вообще сделать всё с минимальным количеством проблем;
Версия Kubernetes — 1.16.6 (впрочем, дальнейшие действия будут аналогичны и для других версий);
Основная задача сводится к тому, чтобы в кластере, развернутом с помощью kubeadm с сервисной подсетью 192.168.0.0/16, заменить её на 172.24.0.0/16.
И так уж совпало, что нам давно было интересно посмотреть, что и как в Kubernetes хранится в etcd, что вообще с этим можно сделать… Вот и подумали: «Почему бы просто не обновить данные в etcd, заменив старые IP-адреса (подсеть) на новые?»
Поискав готовые инструменты для работы с данными в etcd, мы не нашли ничего полностью решающего поставленную задачу. (Кстати, если вы знаете о любых утилитах для работы с данными напрямую в etcd — будем признательны за ссылки.) Однако хорошей отправной точкой стала etcdhelper от OpenShift(спасибо его авторам!).
Эта утилита умеет подключаться к etcd с помощью сертификатов и читать оттуда данные с помощью команд ls, get, dump.
Дописываем etcdhelper
Следующая мысль закономерна: «Что мешает дописать эту утилиту, добавив возможность записи данных в etcd?»
Она воплотилась в модифицированную версию etcdhelper с двумя новыми функциями changeServiceCIDR и changePodCIDR. На её код можно посмотреть здесь.
Что делают новые функции? Алгоритм changeServiceCIDR:
создаем десериализатор;
компилируем регулярное выражение для замены CIDR;
проходим по всем сервисам с типом ClusterIP в кластере:
декодируем значение из etcd в Go-объект;
с помощью регулярного выражения заменяем первые два байта адреса;
присваиваем сервису IP-адрес из новой подсети;
создаем сериализатор, преобразуем Go-объект в protobuf, записываем новые данные в etcd.
Функция changePodCIDR по сути аналогична changeServiceCIDR — только вместо редактирования спецификации сервисов мы делаем это для узла и меняем .spec.PodCIDR на новую подсеть.
Практика
Смена serviceCIDR
План по реализации поставленной задачи — очень простой, но подразумевает даунтайм на момент пересоздания всех pod’ов в кластере. После описания основных шагов мы также поделимся мыслями, как в теории можно минимизировать этот простой.
Подготовительные действия:
установка необходимого ПО и сборка пропатченного etcdhelper;
бэкап etcd и /etc/kubernetes.
Краткий план действий по смене serviceCIDR:
изменение манифестов apiserver’а и controller-manager’а;
перевыпуск сертификатов;
изменение ClusterIP сервисов в etcd;
рестарт всех pod’ов в кластере.
Далее представлена полная последовательность действий в деталях.
Сохраняем себе etcdhelper.go, загружаем зависимости, собираем:
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
4. Меняем сервисную подсеть в манифестах Kubernetes control plane. В файлах /etc/kubernetes/manifests/kube-apiserver.yaml и /etc/kubernetes/manifests/kube-controller-manager.yaml изменяем параметр --service-cluster-ip-range на новую подсеть: 172.24.0.0/16 вместо 192.168.0.0/16.
5. Поскольку мы меняем сервисную подсеть, на которую kubeadm выпускает сертификаты для apiserver’а (в том числе), их необходимо перевыпустить:
Посмотрим, на какие домены и IP-адреса выпущен текущий сертификат:
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
Внимание! В этот момент в кластере перестает работать резолвинг доменов, так как в уже существующих pod’ах в /etc/resolv.conf прописан старый адрес CoreDNS (kube-dns), а kube-proxy изменил правила iptables со старой подсети на новую. Далее в статье написано о возможных вариантах минимизировать простой.
Поправим ConfigMap’ы в пространстве имен kube-system:
kubectl -n kube-system edit cm kubelet-config-1.16
— здесь заменим clusterDNS на новый IP-адрес сервиса kube-dns: kubectl -n kube-system get svc kube-dns.
kubectl -n kube-system edit cm kubeadm-config
— исправим data.ClusterConfiguration.networking.serviceSubnet на новую подсеть.
Так как изменился адрес kube-dns, необходимо обновить конфиг kubelet на всех узлах:
7. Если хотя бы у одного узла оставить старый podCIDR, то kube-controller-manager не сможет запуститься, а pod’ы в кластере не будут планироваться.
На самом деле, изменение podCIDR можно произвести и проще (например, так). Но ведь нам хотелось научиться работать с etcd напрямую, потому что существуют случаи, когда правка объектов Kubernetes в etcd — единственный возможный вариант. (Например, нельзя просто так без простоя изменить у Service поле spec.clusterIP.)
Итог
В статье рассмотрена возможность работы с данными в etcd напрямую, т.е. в обход Kubernetes API. Иногда такой подход позволяет делать «хитрые штуки». Приведенные в тексте операции мы тестировали на реальных K8s-кластерах. Однако их статус готовности к широкому применению — PoC (proof of concept). Поэтому, если вы хотите использовать модифицированную версию утилиты etcdhelper на своих кластерах, делайте это на свой страх и риск.