Kubernetes, ищем базу
В этой статье я хочу показать, что меняется в состоянии кластера kubernetes при создании базовых ресурсов. В качестве кластерной платформы был выбран minikube, чтобы каждый мог повторить проделанные мной шаги безо всяких проблем.
Перед началом работы с кластером была проделана следующая подготовительная работа:
Составления необходимая для сбора логов команда запуска Minikube (все логи можно найти тут):
minikube start \
--vm-driver=docker \
--alsologtostderr \
--extra-config=kubelet.v=6 \
--extra-config=apiserver.v=6 \
--extra-config=controller-manager.v=6 \
--extra-config=scheduler.v=6 \
--extra-config=kube-proxy.v=6 \
--extra-config=etcd.log-level=debug \
--v=10 \
&>minikube.logs
Написаны скрипты просмотра состояния компонентов Kubernetes, а также iptables
Написана простая программа на go для получения ключей и значений etcd
Первым делом запускаем одноузловой кластер при помощи команды выше и получаем ключи etcd, iptables и настройки сетевых интерфейсов базового кластера, в котором нет ничего кроме Control-Plane созданного minikube с помощью kubeadm:
Ключи etcd — ключей много, и полный их список находится здесь, но если обобщить, то происходит базовая настройка кластера, создаются роли, пространства имен, Control-Plane поды и ConfigMaps для конфигурации этих подов.
Iptables — обработанные (убраны все правила связанные с docker, так как они никак не используются) iptables находятся тут, и в основном осуществляют:
Базовую фильтрацию пакетов
Маскарадинг пакетов, о котором поговорим позже
Настройку цепочек до базовых сервисов, а именно CoreDNS и Kube-API-Server (похожие цепочки рассмотрим при создании собственного сервиса)
По итогу Minikube с помощью kubeadm запустил весь control-plane в специальных контейнерах, за которыми следит единственный неконтейнерезированный элемент — kubelet. Стоит отметить, что для мапинга портов непосредственно в сеть хоста в поды Control-Plane был добавлен специальный параметр — hostNetwork: true. Кроме того, при таком типе развертывания кластера менять конфигурацию Control-Plane можно прямо в манифестах расположенных в папке /etc/kubernetes/manifests, и Kubelet автоматически их пересоздаст.
В кластере Minikube по умолчанию были выбраны следующие компоненты: для CNI был выбран bridge, для DNS — CoreDNS, а для CRI — Containerd.
Что такое под и почему для каждого контейнера Control-Plane существует прихвостень с похожим именем разберемся в разделе создания собственного пода.
Касательно настройки сетевых интерфейсов, то существуют как базовые интерфейсы: lo и eth0, так и специализированные для Kubenrentes интерфейсы: bridge и veth
$ ip a
1: lo: state UNKNOWN group default
inet 127.0.0.1/8 scope host lo
2: docker0: state DOWN group default
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
3: bridge: state UP group default
inet 10.244.0.1/16 brd 10.244.255.255 scope global bridge
4: vethe37be523@if2: master bridge state UP group default
352: eth0@if353: state UP group default
inet 192.168.49.2/24 brd 192.168.49.255 scope global eth0
Маршруты тоже базовые, но можно заметить что весь трафик Kubernetes сети (10.244.0.0/16) идет в Bridge:
$ ip r
default via 192.168.49.1 dev eth0
10.244.0.0/16 dev bridge proto kernel scope link src 10.244.0.1
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown
192.168.49.0/24 dev eth0 proto kernel scope link src 192.168.49.2
Что это за интерфейсы такие Bridge и vethe37be523@if2? Пока пропустим объяснение и вернемся к ним, когда будем разворачивать собственный под.
Переходим к созданию первого простого пода:
Ключи etcd — появились только два типа ключей: события по созданию пода, и сам под vault. События нужны только для мониторинга состояния, поэтому сейчас и далее я буду их опускать:
/registry/pods/default/vault
Iptables — появилось новые правила регулирующие исходящий из созданного пода трафик, расскажу о них ниже.
Ничего особого (на первый взгляд) не появилось кроме двух новых контейнеров объединенных в один Pod:
CONTAINER ID | IMAGE | COMMAND | CREATED | STATUS | PORTS | NAMES |
---|---|---|---|---|---|---|
596fbd56d9aa | vault | «docker-entrypoint.s…» | 3 minutes ago | Up 3 minutes | k8s_vault_vault_id… | |
66d72da7ea45 | registry.k8s.io/pause:3.9 | »/pause» | 3 minutes ago | Up 3 minutes | k8s_POD_vault_default_id… |
Зачем создалось два контейнера? Хороший вопрос. Ответ — минимальное количество контейнеров в одном Kubernetes поде — 2, и вот почему. Второй контейнер зовётся pause (вот кстати его исходный код), и после глубокого анализа я выяснил, что нужен он для выполнения следующих функций и только (все остальные проблемы, о которых рассказывают в интернете можно спокойно решить и без этого контейнера). Во-первых, резервация Namespaces, для того чтобы при перезапуске контейнера не пересоздавать выделенные для него Namespaces, благодаря тому что Pause контейнер продолжает работать даже при завершении всех других контейнеров пода, а завершить его может только сам Kubernetes (Kubelet). А во-вторых, выполнение функций процесса с PID 1 при создании контейнеров, разделяющих один PID Namespace. Делать в таком случае какой-то определенный контейнер главным не камильфо, так как появляются следующие проблемы:
Если главный контейнер падает, то внутри пода исчезает процесс с PID 1, из-за чего контейнер не сможет вернуться в Namespace после перезапуска, так как некому быть его родителем.
Кто-то должен собирать детей процессов в контейнерах, соответственно необходимо писать такой функционал для каждого главного контейнера, что не удобно.
Если в описании контейнера явно не указывать создание новых namespaces, то новыми (в пределах пода) будут только pid, utc, cgroops, и mnt. Соответственно контейнер будет иметь собственный hostname, файловую систему, а также первый запущенный процесс будет иметь pid равный единице, ну и cgroops указанные в описании контейнера. Если рассматривать контейнер относительно хоста, то новыми namespace также будут net (сетевые интерфейсы) и ipc (очереди сообщений POSIX). Ниже представлены Namespaces выделенные как для пода, так и для контейнера (крутой цикл статей о Linux Namespaces). Почему pause запускается от имени nobody (UID 65535) честно говоря без понятия, единственное объяснение, которое приходит в голову — /containerd-shim-runc-v2 запустил процесс от этого имени, ведь User Namespace пода такое же как и у хоста:
$ lsns
NS TYPE NPROCS PID USER COMMAND
4026534267 mnt 1 6705 65535 /pause
4026534268 uts 1 6705 65535 /pause
4026534269 ipc 4 6705 65535 /pause
4026534270 pid 1 6705 65535 /pause
4026534271 net 4 6705 65535 /pause
4026534341 cgroup 1 6705 65535 /pause
4026534344 mnt 3 6922 root
4026534345 uts 3 6922 root
4026534346 pid 3 6922 root
4026534347 cgroup 3 6922 root
Файловая система берется из образа контейнера и каталог монтиорвания можно увидеть, если посмотреть на вывод mount внутри контейнера. Из вывода команды ниже можно заметить, что в контейнер по умолчанию также монтируются /etc/hosts, /etc/resolv.conf и /etc/hostname для работы DNS:
$ mount
overlay on / type overlay (
rw,
seclabel,
relatime,
lowerdir=/var/lib/docker/overlay2/l/HADIUDBRK6K6CA2EAYIPIN54CF:
/var/lib/docker/overlay2/l/ZEDDRHSJ5IEREEVGU2PV2YSNGI:
/var/lib/docker/overlay2/l/DEEBZ4NIHQEJX7VSYESA2YWCAJ:
/var/lib/docker/overlay2/l/CPE4IDREOEF54DX2XOUTMT3U2S:
/var/lib/docker/overlay2/l/FQBHQ3BPW6VV3YPG4WEKW4WMEB:
/var/lib/docker/overlay2/l/6IPXNYC6WPK4IGINXKMNVIQ4BH,
upperdir=/var/lib/docker/overlay2/443259d564638ef254e193583dbc23bd5e5a49f5930dd710edff45177454cc9a/diff,
workdir=/var/lib/docker/overlay2/443259d564638ef254e193583dbc23bd5e5a49f5930dd710edff45177454cc9a/work)
/dev/nvme0n1p3 on /etc/resolv.conf type btrfs (...)
/dev/nvme0n1p3 on /etc/hostname type btrfs (...)
/dev/nvme0n1p3 on /etc/hosts type btrfs (...)
LowerDir — включает файловые системы всех слоев внутри контейнера, кроме последнего, UpperDir — файловая система самого верхнего слоя контейнера где отображаются любые изменения во время выполнения, WorkDir — внутренний рабочий каталог, используемый для управления файловой системой (задается в Dockerfile).
Возвращаемся к вопросу о новых сетевых интерфейсах. Bridge является связующим звеном через которое идет весь внутренний трафик Kubernetes, как видно из вывода команды ниже он перенаправляет трафик на veth интерфейсы создаваемые для общения с другими NET Namespaces, в данном случае данный отдельный NET Namespace имеет созданный нами контейнер (еще CoreDNS контейнер, но принципиально они не отличается). Что касается компонентов Control-Plane, то они разворачиваются в NET Namespace хоста, а значит никаких veth не требуют:
bridge link show
5: veth2d3d80cb@if2@docker0: mtu 1500 master bridge state forwarding
Внутри пода есть связанный с veth eth0 интерфейс, на который перенаправляется весь трафик во внешнюю относительно пода сеть. Внутри же пода контейнеры общаются через localhost:
$ ip a
1: lo: state UNKNOWN
inet 127.0.0.1/8 scope host lo
2: eth0@if5: state UP
inet 10.244.0.3/16 brd 10.244.255.255 scope global eth0
$ ip r
default via 10.244.0.1 dev eth0
10.244.0.0/16 dev eth0 scope link src 10.244.0.3
Что касается режима CNI — bridge, то у него есть один существенный минус, а именно он плохо работает в многоузловом режиме, и если необходимо отправлять трафик на контейнер на другом узле, то нужно выбирать другой CNI, например kindnet или calico. Благо Minikube делает это за нас.
Из iptables можно сделать вывод, что весь исходящий трафик контейнера идущий во внешнюю сеть меняет адрес отправителя на адрес сетевого интерфейса (функционал MASQUERADE):
:CNI-1f120c2dca3af9923d1e29d4 - [0:0] -- "Цепочка контейнера vault"
# Если пакет с ip отправителя 10.244.0.3, то перейти в цепочку контейнера vault
-A POSTROUTING -s 10.244.0.3/32 -j CNI-1f120c2dca3af9923d1e29d4
# Если пакет идет в сеть Kubernetes, то MASQUERADE не нужен
-A CNI-1f120c2dca3af9923d1e29d4 -d 10.244.0.0/16 -j ACCEPT
# Если пакет идет не в сеть Kubernetes, то выполнение MASQUERADE
-A CNI-1f120c2dca3af9923d1e29d4 ! -d 224.0.0.0/4 -j MASQUERADE
Касательно того как был создан контейнер (не под, о том как создается под в интернете куча информации) то вот схема:
Kubelet приходит уведомление, что на его узел запланирован под.
Kubelet обращается к Kube-API и берет описание пода.
С помощью CRI, в моем случае это Containerd, первым делом качается образы контейнеров из репозитория (если их нет локально), после чего отправляется запрос на создание контейнеров пода, включая pause.
Containerd в свою очередь через специальный интерфейс — containerd-shim-runc-v2 отправляет запрос на создание контейнера RunС.
RunС является самым нижним уровнем, который непосредственно и запускает процессы в новых Namespace (синоним контейнеров).
Далее для созданного пода был поднят сервис:
Ключи etcd — появились новые enpointslices, endpoints и сервис:
/registry/endpointslices/default/vault-wgjvs
/registry/services/endpoints/default/vault
/registry/services/specs/default/vault
Iptables — появились новые правила перенаправляющие трафик, идущий к сервису, в соответствующий под. А именно создалось две цепочки одна для Cluster IP сервиса, а другая для NodePort. Из чего можно сделать вывод, что service (если он типа NodePort) — это 11 строчек в iptables, за которыми следит kube-proxy:
:KUBE-EXT-VUGVVJAHU4RL2TIW - [0:0] -- "NodePort цепочка"
:KUBE-SEP-6X3WIXWFLXPKXKEP - [0:0] -- "Endpoints цепочка"
:KUBE-SVC-VUGVVJAHU4RL2TIW - [0:0] -- "Clusert IP цепочка"
#Направление пакета в Clusert IP цепочку если адрес назначения 10.104.205.29
-A KUBE-SERVICES -d 10.104.205.29/32 -p tcp -m tcp --dport 8200 -j KUBE-SVC-VUGVVJAHU4RL2TIW
#Отправление пакета не из сети Kubernetes, идущих в Service, в цепочку для MASQUERADE
-A KUBE-SVC-VUGVVJAHU4RL2TIW ! -s 10.244.0.0/16 -d 10.104.205.29/32 -p tcp -m comment --comment "default/vault cluster IP" -m tcp --dport 8200 -j KUBE-MARK-MASQ
#Направление пакета в Endpoints цепочку
-A KUBE-SVC-VUGVVJAHU4RL2TIW -m comment --comment "default/vault -> 10.244.0.3:8200" -j KUBE-SEP-6X3WIXWFLXPKXKEP
#Отправление пакета контейнера в цепочку для MASQUERADE
-A KUBE-SEP-6X3WIXWFLXPKXKEP -s 10.244.0.3/32 -m comment --comment "default/vault" -j KUBE-MARK-MASQ
#Изменение адрса получателя на 10.244.0.3:8200 (ip контейнера)
-A KUBE-SEP-6X3WIXWFLXPKXKEP -p tcp -m comment --comment "default/vault" -m tcp -j DNAT --to-destination 10.244.0.3:8200
#Направление пакета в NodePort цепочку если порт назначения 30074
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/vault" -m tcp --dport 30074 -j KUBE-EXT-VUGVVJAHU4RL2TIW
#Отправление всех пакетов в цепочку для MASQUERADE
-A KUBE-EXT-VUGVVJAHU4RL2TIW -m comment --comment "masquerade traffic for default/vault external destinations" -j KUBE-MARK-MASQ
#Направление пакета в Cluster IP цепочку
-A KUBE-EXT-VUGVVJAHU4RL2TIW -j KUBE-SVC-VUGVVJAHU4RL2TIW
Пояснение касательно KUBE-MARK-MASQ цепочки, более подробное объяснение такому особому подходу к MASQUERADE можно найти тут. Ниже моя личная интерпретация:
# Маркировака пакета для дальнейшего MASQUERADE
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
# Если пакет не промаркирован, то выйти из KUBE-POSTROUTING цепочки
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
# Честно говоря, без понятия
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
# Изменение ардреса источника пакета на адрес сетевого интерфейса
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully
Далее средствами Minikube создан кластер с двумя узлами:
Ключи etcd — появились ресурсы для функционирования KindNet подов, а также отдельные ресурсы для присоединения рабочего узла:
#Дублирование ресурсов для второго узла
/registry/certificatesigningrequests/csr-x6n7c
/registry/csinodes/minikube-m02
/registry/leases/kube-node-lease/minikube-m02
/registry/minions/minikube-m02
/registry/secrets/kube-system/bootstrap-token-u7p7xm
#Ресурсы для работы KindNet
/registry/clusterrolebindings/kindnet
/registry/clusterroles/kindnet
/registry/controllerrevisions/kube-system/kindnet-575d9d6996
/registry/daemonsets/kube-system/kindnet
/registry/pods/kube-system/kindnet-5xc7w
/registry/pods/kube-system/kindnet-8fd58
/registry/pods/kube-system/kube-proxy-v78ms
/registry/serviceaccounts/kube-system/kindnet
Iptables — были убраны правила обработки исходящего трафика контейнеров и заменены на похожие правила kind-masq-agent.
Так как теперь локальный Kubernetes трафик необходимо пересылать за пределы узла, то был развернут DaemonSet с подами KindNet, из-за чего были изменены iptables узлов (iptabels у master и minion абсолютно одинаковые, потому что master также используется как второй minion). Что касательно новой цепочки, то функционально от цепочек CNI-
:KIND-MASQ-AGENT - [0:0] -- "Цепочка для MASQUERADE"
# Отправление пакета, идущего во внешнюю сеть, в цепочку для MASQUERADE
-A POSTROUTING -m addrtype ! --dst-type LOCAL -m comment --comment "kind-masq-agent: ensure nat POSTROUTING directs all non-LOCAL destination traffic to our custom KIND-MASQ-AGENT chain" -j KIND-MASQ-AGENT
# Если пакет идет в сеть Kubernetes, то MASQUERADE не нужен
-A KIND-MASQ-AGENT -d 10.244.0.0/16 -m comment --comment "kind-masq-agent: local traffic is not subject to MASQUERADE" -j RETURN
# Если пакет идет не в сеть Kubernetes, то выполнение MASQUERADE
-A KIND-MASQ-AGENT -m comment --comment "kind-masq-agent: outbound traffic is subject to MASQUERADE (must be last in chain)" -j MASQUERADE
Если присмотреться к сетевой конфигурации, то можно заметить, что Bridge интерфейса больше нет, что и ожидалось, ведь теперь функции CNI выполняет KindNet:
$ ip a
1: lo: state UNKNOWN group default
inet 127.0.0.1/8 scope host lo
2: docker0: state DOWN group default
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
3: veth70124147@if2: state UP group default
inet 10.244.0.1/32 scope global veth70124147
340: eth0@if341: state UP group default
inet 192.168.49.2/24 brd 192.168.49.255 scope global eth0
$ ip r
default via 192.168.49.1 dev eth0
10.244.0.2 dev veth70124147 scope host
10.244.1.0/24 via 192.168.49.3 dev eth0
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown
192.168.49.0/24 dev eth0 proto kernel scope link src 192.168.49.2
Создан Deployment с двумя репликами одного Pod:
Ключи etcd — появились ключи развертывания, набора реплик и двух подов:
/registry/deployments/default/react-d
/registry/pods/default/react-d-86c848f59-7mwt4
/registry/pods/default/react-d-86c848f59-qj6vq
/registry/replicasets/default/react-d-86c848f59
iptables — новых правил не появилось.
Особенности касательно создания пода мы обсудили еще в первом пункте. Кроме же новых контейнеров поменялась сетевая конфигурация, а именно теперь трафик обрабатывается по другому, если раньше весь трафик отправлялся в bridge, то теперь трафик обрабатывается и отправляется в специальный veth согласно сконфигурированным KindNet маршрутам:
$ ip a
...
4: veth79f7eec0@if2: inet 10.244.0.1/32 scope global veth79f7eec0
$ ip r
...
10.244.0.3 dev veth79f7eec0 scope host
Создан Service для Deployment:
Ключи etcd — в etcd все аналогично созданию service в одноузловом кластере
iptables — новые правила практически аналогичны одноузловому кластеру, разве что теперь iptables рабочих узлов должны периодически синхронизироваться для правильной обработки трафика. Внизу представлены новые изменения цепочки Cluster IP (при создании развертывания в одноузловом режиме, изменения правил были бы такими же):
:KUBE-SEP-E7QDI4WX3A3GWTGP - [0:0] -- "Endpoints цепочка до контейнера на minion узле"
:KUBE-SEP-M6LBIRCLGOFYIL5C - [0:0] -- "Endpoints цепочка до контейнера на master узле"
:KUBE-SVC-LPLI5PP7N5LEMN6C - [0:0] -- "Cluster IP цепочка"
# Новое правило, которое с вероятностью 50% отправляет пакет на один Endpoint, да вот она ваша балансировка
-A KUBE-SVC-LPLI5PP7N5LEMN6C -m comment --comment "default/react-np:http -> 10.244.0.3:3000" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-M6LBIRCLGOFYIL5C
# Если 50% не прокнуло, то отправляется на другой Enpoint
-A KUBE-SVC-LPLI5PP7N5LEMN6C -m comment --comment "default/react-np:http -> 10.244.1.2:3000" -j KUBE-SEP-E7QDI4WX3A3GWTGP
На этом пожалуй все, если будут какие–то предложения/исправления от знатоков, пишите в комментарии, поправлю.