Наш опыт работы с данными в 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'ов в кластере.
Далее представлена полная последовательность действий в деталях.
1. Устанавливаем etcd-client для дампа данных:
apt install etcd-client
2. Собираем etcdhelper:
- Ставим 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
- Сохраняем себе
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
3. Делаем бэкап 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. Меняем сервисную подсеть в манифестах 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
- Подготовим минимальный конфиг для 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" # IP-адрес мастер узла
- Удалим старые crt и key, так как без этого новый сертификат не выпустится:
rm /etc/kubernetes/pki/apiserver.{key,crt}
- Перевыпустим сертификаты для API-сервера:
kubeadm init phase certs apiserver --config=kubeadm-config.yaml
- Проверим, что сертификат выпустился для новой подсети:
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
- После перевыпуска сертификата API-сервера перезапустим его контейнер:
docker ps | grep k8s_kube-apiserver | awk '{print $1}' | xargs docker restart
- Перегенерируем конфиг для
admin.conf
:kubeadm alpha certs renew admin.conf
- Отредактируем данные в 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
Внимание! В этот момент в кластере перестает работать резолвинг доменов, так как в уже существующих 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 на всех узлах:
kubeadm upgrade node phase kubelet-config && systemctl restart kubelet
- Осталось перезапустить все pod'ы в кластере:
kubectl get pods --no-headers=true --all-namespaces |sed -r 's/(\S+)\s+(\S+).*/kubectl --namespace \1 delete pod \2/e'
Минимизация простоя
Мысли, как можно минимизировать даунтайм:
- После изменений манифестов control plane’а создать новый сервис kube-dns, к примеру, с названием
kube-dns-tmp
и новым адресом172.24.0.10
. - Сделать
if
в etcdhelper, который не будет модифицировать сервис kube-dns. - Заменить во всех kubelet'ах адрес
ClusterDNS
на новый, при этом старый сервис продолжит работать одновременно с новым. - Дождаться, пока pod'ы с приложениями перекатятся либо сами по естественным причинам, либо в согласованное время.
- Удалить сервис
kube-dns-tmp
и поменятьserviceSubnetCIDR
для сервиса kube-dns.
Этот план позволит минимизировать даунтайм до ~минуты — на время удаления сервиса kube-dns-tmp
и замены подсети для сервиса kube-dns
.
Модификация podNetwork
Заодно мы решили посмотреть, как модифицировать podNetwork с помощью получившегося etcdhelper'а. Последовательность действий получается следующей:
- исправляем конфиги в
kube-system
; - исправляем манифест kube-controller-manager'а;
- изменяем podCIDR напрямую в etcd;
- перезагружаем все узлы кластера.
Теперь подробнее об этих действиях:
1. Модифицируем ConfigMap'ы в пространстве имен kube-system
:
kubectl -n kube-system edit cm kubeadm-config
— исправляем data.ClusterConfiguration.networking.podSubnet
на новую подсеть 10.55.0.0/16
.
kubectl -n kube-system edit cm kube-proxy
— исправляем data.config.conf.clusterCIDR: 10.55.0.0/16
.
2. Модифицируем манифест controller-manager'а:
vim /etc/kubernetes/manifests/kube-controller-manager.yaml
— исправляем --cluster-cidr=10.55.0.0/16
.
3. Смотрим на текущие значения .spec.podCIDR
, .spec.podCIDRs
, .InternalIP
, .status.addresses
для всех узлов кластера:
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. Заменим podCIDR, внеся правки напрямую в 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. Проверим, что podCIDR действительно изменился:
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. По очереди перезагрузим все узлы кластера.
7. Если хотя бы у одного узла оставить старый podCIDR, то kube-controller-manager не сможет запуститься, а pod'ы в кластере не будут планироваться.
На самом деле, изменение podCIDR можно произвести и проще (например, так). Но ведь нам хотелось научиться работать с etcd напрямую, потому что существуют случаи, когда правка объектов Kubernetes в etcd — единственный возможный вариант. (Например, нельзя просто так без простоя изменить у Service поле spec.clusterIP
.)
Итог
В статье рассмотрена возможность работы с данными в etcd напрямую, т.е. в обход Kubernetes API. Иногда такой подход позволяет делать «хитрые штуки». Приведенные в тексте операции мы тестировали на реальных K8s-кластерах. Однако их статус готовности к широкому применению — PoC (proof of concept). Поэтому, если вы хотите использовать модифицированную версию утилиты etcdhelper на своих кластерах, делайте это на свой страх и риск.
P.S.
Читайте также в нашем блоге: