Наш досвід роботи з даними в etcd Kubernetes кластера безпосередньо (без K8s API)

Все частіше до нас звертаються клієнти з проханням забезпечити доступ у Kubernetes-кластер для можливості звернення до сервісів усередині кластера: щоб можна було безпосередньо підключитися до якоїсь бази даних або сервісу, для зв'язку локальної програми з додатками всередині кластера.

Наш досвід роботи з даними в etcd Kubernetes кластера безпосередньо (без K8s API)

Наприклад, виникає потреба підключитися зі своєї локальної машини до сервісу 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. Як варіант, можуть порадити і таке:

Наступні процеси мають проблему: після того, як configured, підси керуються з old IP як DNS nameserver в /etc/resolv.conf.
Since I still did not find the solution, і 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'а (у тому числі), їх необхідно перевипустити:

  1. Подивимося, на які домени та 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
  2. Підготуємо мінімальний конфіг для 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-адрес мастер узла
  3. Видалимо старі crt і key, тому що без цього новий сертифікат не випуститься:
    rm /etc/kubernetes/pki/apiserver.{key,crt}
  4. Перевипустимо сертифікати для API-сервера:
    kubeadm init phase certs apiserver --config=kubeadm-config.yaml
  5. Перевіримо, що сертифікат випустився для нової підмережі:
    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. Після перевипуску сертифіката API-сервера перезапустимо його контейнер:
    docker ps | grep k8s_kube-apiserver | awk '{print $1}' | xargs docker restart
  7. Перегенеруємо конфіг для admin.conf:
    kubeadm alpha certs renew admin.conf
  8. Відредагуємо дані 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 зі старої підмережі на нову. Далі у статті написано про можливі варіанти мінімізувати простий.

  9. Поправимо 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 на нову підмережу.

  10. Оскільки змінилася адреса kube-dns, необхідно оновити конфіг kubelet на всіх вузлах:
    kubeadm upgrade node phase kubelet-config && systemctl restart kubelet
  11. Залишилося перезапустити всі pod'и в кластері:
    kubectl get pods --no-headers=true --all-namespaces |sed -r 's/(S+)s+(S+).*/kubectl --namespace 1 delete pod 2/e'

Мінімізація простою

Думки, як можна мінімізувати даунтайм:

  1. Після змін маніфестів control plane'а створити новий сервіс kube-dns, наприклад, з назвою kube-dns-tmp та новою адресою 172.24.0.10.
  2. зробити if в etcdhelper, який не модифікуватиме сервіс kube-dns.
  3. Замінити у всіх kubelet'ах адресу ClusterDNS на новий, причому старий сервіс продовжить працювати одночасно з новим.
  4. Дочекатися, доки під'ї з додатками перекотяться або самі з природних причин, або у погоджений час.
  5. Видалити сервіс 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 на своїх кластерах, робіть це на свій страх та ризик.

PS

Читайте також у нашому блозі:

Джерело: habr.com

Додати коментар або відгук