- PVSM.RU - https://www.pvsm.ru -
Прим. перев.: Автор оригинальной статьи — Nicolas Leiva — архитектор решений Cisco, который решил поделиться со своими коллегами, сетевыми инженерами, о том, как устроена сеть Kubernetes изнутри. Для этого он исследует простейшую её конфигурацию в кластере, активно применяя здравый смысл, свои познания о сетях и стандартные утилиты Linux/Kubernetes. Получилось объёмно, зато весьма наглядно.
Помимо того факта, что руководство Kubernetes The Hard Way [1] от Kelsey Hightower просто работает (даже на AWS! [2]), мне понравилось, что сеть поддерживается в чистоте и простоте; и это замечательная возможность понять, какова роль, например, Container Network Interface (CNI [3]). Сказав это, добавлю, что сеть Kubernetes в действительности не очень-то интуитивно понятна, особенно для новичков… а также не забывайте, что «такой вещи, как сети для контейнеров, попросту не существует [4]».
Хотя уже есть неплохие материалы по этой теме (см. ссылки здесь [5]), я не смог найти такого примера, что объединил бы всё необходимое с выводами команд, которые так любят и ненавидят сетевые инженеры, демонстрируя, что же на самом деле происходит за кулисами. Поэтому я и решил собрать информацию из множества источников — надеюсь, это поможет и вам лучше разобраться, как всё связано друг с другом. Эти знания важны не только для того, чтобы проверить себя, но и для упрощения процесса диагностики проблем. Можете последовать приведенному примеру в своём кластере из Kubernetes The Hard Way [1]: все IP-адреса и настройки взяты оттуда (по состоянию коммитов на май 2018 года, до использования контейнеров Nabla [6]).
А начнём мы с конца, когда у нас есть три контроллера и три рабочих узла:
Вы можете заметить, что здесь также есть как минимум три частные подсети! Немного терпения, и все они будут рассмотрены. Помните, что, хоть мы и ссылаемся на очень специфичные IP-префиксы, они попросту взяты из Kubernetes The Hard Way [1], так что имеют лишь локальную значимость, а вы вольны выбрать для своего окружения любой другой блок адресов в соответствии с RFC 1918 [7]. Для случая с IPv6 будет отдельная статья в блоге.
Это внутренняя сеть, частью которой являются все узлы. Определяется флагом --private-network-ip
в GCP [8] или опцией --private-ip-address
в AWS [9] при выделении вычислительных ресурсов.
for i in 0 1 2; do
gcloud compute instances create controller-${i}
# ...
--private-network-ip 10.240.0.1${i}
# ...
done
(controllers_gcp.sh
[10])
for i in 0 1 2; do
declare controller_id${i}=`aws ec2 run-instances
# ...
--private-ip-address 10.240.0.1${i}
# ...
done
(controllers_aws.sh
[11])
Каждый экземпляр будет иметь два IP-адреса: частный от сети узла (контроллеры — 10.240.0.1${i}/24
, воркеры — 10.240.0.2${i}/24
) и публичный, назначенный облачным провайдером, о котором мы поговорим попозже, как доберёмся до NodePorts
.
$ gcloud compute instances list
NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS
controller-0 us-west1-c n1-standard-1 10.240.0.10 35.231.XXX.XXX RUNNING
worker-1 us-west1-c n1-standard-1 10.240.0.21 35.231.XX.XXX RUNNING
...
$ aws ec2 describe-instances --query 'Reservations[].Instances[].[Tags[?Key==`Name`].Value[],PrivateIpAddress,PublicIpAddress]' --output text | sed '$!N;s/n/ /'
10.240.0.10 34.228.XX.XXX controller-0
10.240.0.21 34.173.XXX.XX worker-1
...
Все узлы должны быть способны пинговать друг друга, если политики безопасности верны [12] (и если ping
установлен на хост).
Это сеть, в которой живут поды. Каждый рабочий узел использует подсеть этой сети. В нашем случае — POD_CIDR=10.200.${i}.0/24
для worker-${i}
.
Чтобы понять, как всё настроено, сделаем шаг назад и посмотрим на сетевую модель Kubernetes [13], которая требует следующего:
Реализация всего этого возможна разными способами, и Kubernetes передаёт сетевую настройку плагину CNI [14].
«Плагин CNI отвечает за добавление сетевого интерфейса в сетевом пространстве имён контейнера (например, один конец veth-пары) и выполнение необходимых изменений на хосте (например, подключение второго конца veth к мосту). Затем он должен назначить IP интерфейсу и настроить маршруты соответственно разделу «IP Address Management» путём вызова нужного плагина IPAM». (из Container Network Interface Specification [15])
«Пространство имён оборачивает глобальный системный ресурс в абстракцию, которая видна процессам в этом пространстве имён таким образом, что у них есть свой изолированный экземпляр глобального ресурса. Изменения в глобальном ресурсе видны другим процессам, входящим в это пространство имён, но не видны иным процессам». (из man-страницы namespaces [16])
Linux предоставляет семь различных пространств имён (Cgroup
, IPC
, Network
, Mount
, PID
, User
, UTS
). Сетевые (Network
) пространства имён (CLONE_NEWNET
) определяют сетевые ресурсы, которые доступны процессу: «У каждого сетевого пространства имён есть свои сетевые устройства, IP-адреса, таблицы IP-маршрутизации, директория /proc/net
, номера портов и так далее» (из статьи «Namespaces in operation [17]»).
«Виртуальная сетевая пара (veth) предлагает абстракцию в виде „трубы“, которую можно использовать для создания туннелей между сетевыми пространствами имён или же для создания моста к физическому сетевому устройству в ином сетевом пространстве. Когда пространство имён освобождается, все находящиеся в нём veth-устройства уничтожаются». (из man-страницы network namespaces [18])
Спустимся на землю и посмотрим, как всё это относится к кластеру. Во-первых, сетевые плагины [19] в Kubernetes разнообразны, и плагины CNI — одни из них (почему не CNM? [20]). Kubelet [21] на каждом узле говорит исполняемой среде контейнера, какой сетевой плагин [19] использовать. Container Network Interface (CNI [3]) находится между исполняемой средой контейнера и сетевой реализацией. А уже плагин CNI настраивает сеть.
«Плагин CNI выбирается передачей опции командной строки
--network-plugin=cni
в Kubelet. Kubelet считывает файл из--cni-conf-dir
(по умолчанию это/etc/cni/net.d
) и использует конфигурацию CNI из этого файла для настройки сети каждого пода». (из Network Plugin Requirements [22])
Реальные бинарники плагина CNI находятся в -- cni-bin-dir
(по умолчанию это /opt/cni/bin
).
Обратите внимание, что параметры вызова kubelet.service
[23] включают в себя --network-plugin=cni
:
[Service]
ExecStart=/usr/local/bin/kubelet \
--config=/var/lib/kubelet/kubelet-config.yaml \
--network-plugin=cni \
...
Первым делом Kubernetes создаёт сетевое пространство имён для пода, ещё до вызова каких-либо плагинов. Это реализуется с помощью специального контейнера pause
, который «служит „родительским контейнером“ для всех контейнеров пода» (из статьи «The Almighty Pause Container [24]»). Затем Kubernetes выполняет плагин CNI для присоединения контейнера pause
к сети. Все контейнеры пода используют сетевое пространство имён (netns
) этого pause
-контейнера.
{
"cniVersion": "0.3.1",
"name": "bridge",
"type": "bridge",
"bridge": "cnio0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"ranges": [
[{"subnet": "${POD_CIDR}"}]
],
"routes": [{"dst": "0.0.0.0/0"}]
}
}
Используемый конфиг для CNI [25] указывает на применение плагина bridge
для конфигурации программного моста Linux (L2) в корневом пространстве имён под названием cnio0
(имя по умолчанию [26] — cni0
), который выступает в роли шлюза ("isGateway": true
).
Также будет настроена veth-пара для подключения пода к только что созданному мосту:
Для назначения L3-информации, такой как IP-адреса, вызывается плагин IPAM [27] (ipam
). В данном случае используется тип host-local
, «который хранит состояние локально на файловой системе хоста, чем обеспечивает уникальность IP-адресов на одном хосте» (из описания host-local
[28]). Плагин IPAM возвращает эту информацию предыдущему плагину (bridge
), благодаря чему могут быть настроены все указанные в конфиге маршруты ("routes": [{"dst": "0.0.0.0/0"}]
). Если gw
не указан, он берётся из подсети [29]. Маршрут по умолчанию тоже настраивается в сетевом пространстве имён подов, указывая на мост (который настраивается как первый IP подсети пода).
И последняя важная деталь: мы запросили маскарадинг ("ipMasq": true
) трафика, исходящего из сети подов. В действительности нам здесь не нужен NAT, но таков конфиг в Kubernetes The Hard Way [1]. Поэтому для полноты картины я должен упомянуть, что записи в iptables
плагина bridge
настроены для этого конкретного примера. Все пакеты из пода, получатель которых не входит в диапазон 224.0.0.0/4
, будет за NAT'ом [30], что не совсем соответствует требованию «все контейнеры могут общаться с любыми другими контейнерами без использования NAT». Что ж, мы ещё докажем, почему NAT не нужен…
Теперь мы готовы настраивать поды. Посмотрим на все сетевые пространства имён одного из рабочих узлов и проанализируем одно из них после создания deployment'а nginx
отсюда [32]. Воспользуемся lsns
с опцией -t
для выбора нужного типа пространства имён (т.е. net
):
ubuntu@worker-0:~$ sudo lsns -t net
NS TYPE NPROCS PID USER COMMAND
4026532089 net 113 1 root /sbin/init
4026532280 net 2 8046 root /pause
4026532352 net 4 16455 root /pause
4026532426 net 3 27255 root /pause
С помощью опции -i
к ls
мы можем найти их номера inode:
ubuntu@worker-0:~$ ls -1i /var/run/netns
4026532352 cni-1d85bb0c-7c61-fd9f-2adc-f6e98f7a58af
4026532280 cni-7cec0838-f50c-416a-3b45-628a4237c55c
4026532426 cni-912bcc63-712d-1c84-89a7-9e10510808a0
Также вывести список всех сетевых пространств имён можно с помощью ip netns
:
ubuntu@worker-0:~$ ip netns
cni-912bcc63-712d-1c84-89a7-9e10510808a0 (id: 2)
cni-1d85bb0c-7c61-fd9f-2adc-f6e98f7a58af (id: 1)
cni-7cec0838-f50c-416a-3b45-628a4237c55c (id: 0)
Чтобы увидеть все процессы, запущенные в сетевом пространстве cni-912bcc63–712d-1c84–89a7–9e10510808a0
(4026532426
), можно выполнить, например, такую команду:
ubuntu@worker-0:~$ sudo ls -l /proc/[1-9]*/ns/net | grep 4026532426 | cut -f3 -d"/" | xargs ps -p
PID TTY STAT TIME COMMAND
27255 ? Ss 0:00 /pause
27331 ? Ss 0:00 nginx: master process nginx -g daemon off;
27355 ? S 0:00 nginx: worker process
Видно, что помимо pause
в этом поде мы запустили nginx
. Контейнер pause
делит пространства имён net
и ipc
со всеми остальными контейнерами пода. Запомним PID от pause
— 27255; мы ещё вернёмся к нему.
Теперь посмотрим, что расскажет об этом поде kubectl
:
$ kubectl get pods -o wide | grep nginx
nginx-65899c769f-wxdx6 1/1 Running 0 5d 10.200.0.4 worker-0
Больше подробностей:
$ kubectl describe pods nginx-65899c769f-wxdx6
Name: nginx-65899c769f-wxdx6
Namespace: default
Node: worker-0/10.240.0.20
Start Time: Thu, 05 Jul 2018 14:20:06 -0400
Labels: pod-template-hash=2145573259
run=nginx
Annotations: <none>
Status: Running
IP: 10.200.0.4
Controlled By: ReplicaSet/nginx-65899c769f
Containers:
nginx:
Container ID: containerd://4c0bd2e2e5c0b17c637af83376879c38f2fb11852921b12413c54ba49d6983c7
Image: nginx
...
Мы видим название пода — nginx-65899c769f-wxdx6
— и ID одного из его контейнеров (nginx
), но про pause
— пока что ни слова. Копнём глубже рабочий узел, чтобы сопоставить все данные. Помните, что в Kubernetes The Hard Way [1] не используется Docker [33], поэтому для получения подробностей о контейнере мы обращаемся к консольной утилите containerd [34] — ctr (см. также статью «Интеграция containerd с Kubernetes, заменяющая Docker, готова к production [35]» — прим. перев.):
ubuntu@worker-0:~$ sudo ctr namespaces ls
NAME LABELS
k8s.io
Зная пространство имён containerd (k8s.io
), можно получить ID контейнера nginx
:
ubuntu@worker-0:~$ sudo ctr -n k8s.io containers ls | grep nginx
4c0bd2e2e5c0b17c637af83376879c38f2fb11852921b12413c54ba49d6983c7 docker.io/library/nginx:latest io.containerd.runtime.v1.linux
… и pause
тоже:
ubuntu@worker-0:~$ sudo ctr -n k8s.io containers ls | grep pause
0866803b612f2f55e7b6b83836bde09bd6530246239b7bde1e49c04c7038e43a k8s.gcr.io/pause:3.1 io.containerd.runtime.v1.linux
21640aea0210b320fd637c22ff93b7e21473178de0073b05de83f3b116fc8834 k8s.gcr.io/pause:3.1 io.containerd.runtime.v1.linux
d19b1b1c92f7cc90764d4f385e8935d121bca66ba8982bae65baff1bc2841da6 k8s.gcr.io/pause:3.1 io.containerd.runtime.v1.linux
ID контейнера nginx
, заканчивающийся на …983c7
, совпадает с тем, что мы получили от kubectl
. Посмотрим, получится ли выяснить, какой pause
-контейнер принадлежит поду nginx
:
ubuntu@worker-0:~$ sudo ctr -n k8s.io task ls
TASK PID STATUS
...
d19b1b1c92f7cc90764d4f385e8935d121bca66ba8982bae65baff1bc2841da6 27255 RUNNING
4c0bd2e2e5c0b17c637af83376879c38f2fb11852921b12413c54ba49d6983c7 27331 RUNNING
Помните, что процессы с PID 27331 и 27355 запущены в сетевом пространстве имён cni-912bcc63–712d-1c84–89a7–9e10510808a0
?
ubuntu@worker-0:~$ sudo ctr -n k8s.io containers info d19b1b1c92f7cc90764d4f385e8935d121bca66ba8982bae65baff1bc2841da6
{
"ID": "d19b1b1c92f7cc90764d4f385e8935d121bca66ba8982bae65baff1bc2841da6",
"Labels": {
"io.cri-containerd.kind": "sandbox",
"io.kubernetes.pod.name": "nginx-65899c769f-wxdx6",
"io.kubernetes.pod.namespace": "default",
"io.kubernetes.pod.uid": "0b35e956-8080-11e8-8aa9-0a12b8818382",
"pod-template-hash": "2145573259",
"run": "nginx"
},
"Image": "k8s.gcr.io/pause:3.1",
...
… и:
ubuntu@worker-0:~$ sudo ctr -n k8s.io containers info 4c0bd2e2e5c0b17c637af83376879c38f2fb11852921b12413c54ba49d6983c7
{
"ID": "4c0bd2e2e5c0b17c637af83376879c38f2fb11852921b12413c54ba49d6983c7",
"Labels": {
"io.cri-containerd.kind": "container",
"io.kubernetes.container.name": "nginx",
"io.kubernetes.pod.name": "nginx-65899c769f-wxdx6",
"io.kubernetes.pod.namespace": "default",
"io.kubernetes.pod.uid": "0b35e956-8080-11e8-8aa9-0a12b8818382"
},
"Image": "docker.io/library/nginx:latest",
...
Теперь мы точно знаем, какие контейнеры запущены в этом поде (nginx-65899c769f-wxdx6
) и сетевом пространстве имён (cni-912bcc63–712d-1c84–89a7–9e10510808a0
):
4c0bd2e2e5c0b17c637af83376879c38f2fb11852921b12413c54ba49d6983c7
);d19b1b1c92f7cc90764d4f385e8935d121bca66ba8982bae65baff1bc2841da6
).
Как же этот под (nginx-65899c769f-wxdx6
) подключён к сети? Воспользуемся полученным ранее PID 27255 от pause
для запуска команд в его сетевом пространстве имён (cni-912bcc63–712d-1c84–89a7–9e10510808a0
):
ubuntu@worker-0:~$ sudo ip netns identify 27255
cni-912bcc63-712d-1c84-89a7-9e10510808a0
Для этих целей воспользуемся nsenter
с опцией -t
, определяющей целевой PID, и -n
без указания файла, чтобы попасть в сетевое пространство имён целевого процесса (27255). Вот что скажет ip link show
:
ubuntu@worker-0:~$ sudo nsenter -t 27255 -n ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
3: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 0a:58:0a:c8:00:04 brd ff:ff:ff:ff:ff:ff link-netnsid 0
… и ifconfig eth0
:
ubuntu@worker-0:~$ sudo nsenter -t 27255 -n ifconfig eth0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.200.0.4 netmask 255.255.255.0 broadcast 0.0.0.0
inet6 fe80::2097:51ff:fe39:ec21 prefixlen 64 scopeid 0x20<link>
ether 0a:58:0a:c8:00:04 txqueuelen 0 (Ethernet)
RX packets 540 bytes 42247 (42.2 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 177 bytes 16530 (16.5 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
Таким образом подтверждается, что IP-адрес, полученный ранее через kubectl get pod
, настроен на интерфейсе eth0
пода. Этот интерфейс — часть veth-пары, один из концов которой в поде, а второй — в корневом пространстве имён. Чтобы узнать интерфейс второго конца, воспользуемся ethtool
:
ubuntu@worker-0:~$ sudo ip netns exec cni-912bcc63-712d-1c84-89a7-9e10510808a0 ethtool -S eth0
NIC statistics:
peer_ifindex: 7
Видим, что ifindex
пира — 7. Проверим, что он в корневом пространстве имён. Это можно сделать с помощью ip link
:
ubuntu@worker-0:~$ ip link | grep '^7:'
7: veth71f7d238@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master cnio0 state UP mode DEFAULT group default
Чтобы убедиться в этом окончательно, посмотрим:
ubuntu@worker-0:~$ sudo cat /sys/class/net/veth71f7d238/ifindex
7
Отлично, с виртуальным линком теперь всё понятно. С помощью brctl
посмотрим, кто ещё подключён к Linux-мосту:
ubuntu@worker-0:~$ brctl show cnio0
bridge name bridge id STP enabled interfaces
cnio0 8000.0a580ac80001 no veth71f7d238
veth73f35410
vethf273b35f
Итак, картина выглядит следующим образом:
Как мы на самом деле пересылаем трафик? Посмотрим на таблицу маршрутизации в сетевом пространстве имён пода:
ubuntu@worker-0:~$ sudo ip netns exec cni-912bcc63-712d-1c84-89a7-9e10510808a0 ip route show
default via 10.200.0.1 dev eth0
10.200.0.0/24 dev eth0 proto kernel scope link src 10.200.0.4
По меньшей мере известно, как добраться до корневого пространства имён (default via 10.200.0.1
). Теперь посмотрим таблицу маршрутизации хоста:
ubuntu@worker-0:~$ ip route list
default via 10.240.0.1 dev eth0 proto dhcp src 10.240.0.20 metric 100
10.200.0.0/24 dev cnio0 proto kernel scope link src 10.200.0.1
10.240.0.0/24 dev eth0 proto kernel scope link src 10.240.0.20
10.240.0.1 dev eth0 proto dhcp scope link src 10.240.0.20 metric 100
Мы знаем, как пересылать пакеты на VPC Router (у VPC есть [36] «скрытый» [implicit] роутер, который обычно имеет второй адрес [37] из основного пространства IP-адресов подсети). Теперь: знает ли VPC Router, как добраться до сети каждого пода? Нет, не знает, поэтому предполагается, что маршруты будут настроены плагином CNI или же вручную [38] (как в руководстве). По всей видимости, AWS CNI-plugin [39] делает именно это за нас в AWS. Помните, что есть множество плагинов CNI [40], а мы рассматриваем пример простейшей сетевой конфигурации:
Командой kubectl create -f busybox.yaml
создадим два идентичных контейнера busybox
с Replication Controller:
apiVersion: v1
kind: ReplicationController
metadata:
name: busybox0
labels:
app: busybox0
spec:
replicas: 2
selector:
app: busybox0
template:
metadata:
name: busybox0
labels:
app: busybox0
spec:
containers:
- image: busybox
command:
- sleep
- "3600"
imagePullPolicy: IfNotPresent
name: busybox
restartPolicy: Always
(busybox.yaml
[41])
Получим:
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
busybox0-g6pww 1/1 Running 0 4s 10.200.1.15 worker-1
busybox0-rw89s 1/1 Running 0 4s 10.200.0.21 worker-0
...
Пинги от одного контейнера к другому должны быть успешными:
$ kubectl exec -it busybox0-rw89s -- ping -c 2 10.200.1.15
PING 10.200.1.15 (10.200.1.15): 56 data bytes
64 bytes from 10.200.1.15: seq=0 ttl=62 time=0.528 ms
64 bytes from 10.200.1.15: seq=1 ttl=62 time=0.440 ms
--- 10.200.1.15 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.440/0.484/0.528 ms
Для понимания движения трафика можно посмотреть на пакеты с помощью tcpdump
или conntrack
:
ubuntu@worker-0:~$ sudo conntrack -L | grep 10.200.1.15
icmp 1 29 src=10.200.0.21 dst=10.200.1.15 type=8 code=0 id=1280 src=10.200.1.15 dst=10.240.0.20 type=0 code=0 id=1280 mark=0 use=1
IP источника с пода 10.200.0.21 транслируется в IP-адрес узла 10.240.0.20.
ubuntu@worker-1:~$ sudo conntrack -L | grep 10.200.1.15
icmp 1 28 src=10.240.0.20 dst=10.200.1.15 type=8 code=0 id=1280 src=10.200.1.15 dst=10.240.0.20 type=0 code=0 id=1280 mark=0 use=1
В iptables можно увидеть, что счётчики увеличиваются:
ubuntu@worker-0:~$ sudo iptables -t nat -Z POSTROUTING -L -v
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
...
5 324 CNI-be726a77f15ea47ff32947a3 all -- any any 10.200.0.0/24 anywhere /* name: "bridge" id: "631cab5de5565cc432a3beca0e2aece0cef9285482b11f3eb0b46c134e457854" */
Zeroing chain `POSTROUTING'
С другой стороны, если убрать "ipMasq": true
из конфигурации плагина CNI, можно увидеть следующее (эта операция выполняется исключительно для образовательных целей — не рекомендуем менять конфиг на работающем кластере!):
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
busybox0-2btxn 1/1 Running 0 16s 10.200.0.15 worker-0
busybox0-dhpx8 1/1 Running 0 16s 10.200.1.13 worker-1
...
Пинг всё ещё должен проходить:
$ kubectl exec -it busybox0-2btxn -- ping -c 2 10.200.1.13
PING 10.200.1.6 (10.200.1.6): 56 data bytes
64 bytes from 10.200.1.6: seq=0 ttl=62 time=0.515 ms
64 bytes from 10.200.1.6: seq=1 ttl=62 time=0.427 ms
--- 10.200.1.6 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.427/0.471/0.515 ms
И в данном случае — без использования NAT:
ubuntu@worker-0:~$ sudo conntrack -L | grep 10.200.1.13
icmp 1 29 src=10.200.0.15 dst=10.200.1.13 type=8 code=0 id=1792 src=10.200.1.13 dst=10.200.0.15 type=0 code=0 id=1792 mark=0 use=1
Итак, мы проверили, что «все контейнеры могут общаться с любыми другими контейнерами без использования NAT».
ubuntu@worker-1:~$ sudo conntrack -L | grep 10.200.1.13
icmp 1 27 src=10.200.0.15 dst=10.200.1.13 type=8 code=0 id=1792 src=10.200.1.13 dst=10.200.0.15 type=0 code=0 id=1792 mark=0 use=1
Возможно, вы заметили в примере с busybox
, что IP-адреса, выделенные для пода busybox
, были разными в каждом случае. Что, если бы мы хотели сделать эти контейнеры доступными для связи со стороны других подов? Можно было бы взять текущие IP-адреса пода, но они изменятся. По этой причине нужно настроить ресурс Service
, который будет проксировать запросы ко множеству недолговечных подов.
«Service в Kubernetes — абстракция, определяющая логическую совокупность подов и политику, по которой к ним можно обращаться». (из документации Kubernetes Services [42])
Есть разные способы публикации сервиса; типом по умолчанию является ClusterIP
, настраивающий IP-адрес из CIDR-блока кластера (т.е. доступного только из кластера). Одним из таких примеров является DNS Cluster Add-on, настроенный в Kubernetes The Hard Way.
# ...
apiVersion: v1
kind: Service
metadata:
name: kube-dns
namespace: kube-system
labels:
k8s-app: kube-dns
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
kubernetes.io/name: "KubeDNS"
spec:
selector:
k8s-app: kube-dns
clusterIP: 10.32.0.10
ports:
- name: dns
port: 53
protocol: UDP
- name: dns-tcp
port: 53
protocol: TCP
# ...
(kube-dns.yaml
[43])
kubectl
показывает, что Service
помнит об endpoint'ах и делает их трансляцию:
$ kubectl -n kube-system describe services
...
Selector: k8s-app=kube-dns
Type: ClusterIP
IP: 10.32.0.10
Port: dns 53/UDP
TargetPort: 53/UDP
Endpoints: 10.200.0.27:53
Port: dns-tcp 53/TCP
TargetPort: 53/TCP
Endpoints: 10.200.0.27:53
...
Как именно?.. Снова iptables
. Пройдёмся по правилам, созданным для этого примера. Их полный список можно увидеть командой iptables-save
.
Как только пакеты создаются процессом (OUTPUT
) или приходят на сетевой интерфейс (PREROUTING
), они проходят через следующие цепочки iptables
:
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
Следующие цели соответствуют TCP-пакетам, отправленным на 53-й порт у 10.32.0.10, и транслируются получателю 10.200.0.27 с 53-м портом:
-A KUBE-SERVICES -d 10.32.0.10/32 -p tcp -m comment --comment "kube-system/kube-dns:dns-tcp cluster IP" -m tcp --dport 53 -j KUBE-SVC-ERIFXISQEP7F7OF4
-A KUBE-SVC-ERIFXISQEP7F7OF4 -m comment --comment "kube-system/kube-dns:dns-tcp" -j KUBE-SEP-32LPCMGYG6ODGN3H
-A KUBE-SEP-32LPCMGYG6ODGN3H -p tcp -m comment --comment "kube-system/kube-dns:dns-tcp" -m tcp -j DNAT --to-destination 10.200.0.27:53
То же самое для UDP-пакетов (получатель 10.32.0.10:53 → 10.200.0.27:53):
-A KUBE-SERVICES -d 10.32.0.10/32 -p udp -m comment --comment "kube-system/kube-dns:dns cluster IP" -m udp --dport 53 -j KUBE-SVC-TCOU7JCQXEZGVUNU
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns" -j KUBE-SEP-LRUTK6XRXU43VLIG
-A KUBE-SEP-LRUTK6XRXU43VLIG -p udp -m comment --comment "kube-system/kube-dns:dns" -m udp -j DNAT --to-destination 10.200.0.27:53
Есть и другие типы Services
в Kubernetes. В частности, в Kubernetes The Hard Way рассказано про NodePort
— см. Smoke Test: Services [44].
kubectl expose deployment nginx --port 80 --type NodePort
NodePort
публикует сервис на IP-адресе каждого узла, размещая его на статический порт (он и называется NodePort
). К сервису NodePort
можно обратиться и извне кластера. Проверить выделенный порт (в данном случае — 31088) можно с помощью kubectl
:
$ kubectl describe services nginx
...
Type: NodePort
IP: 10.32.0.53
Port: <unset> 80/TCP
TargetPort: 80/TCP
NodePort: <unset> 31088/TCP
Endpoints: 10.200.1.18:80
...
Под теперь доступен из интернета как http://${EXTERNAL_IP}:31088/
. Здесь EXTERNAL_IP
— публичный IP-адрес любого рабочего экземпляра. В этом примере я использовал публичный IP-адрес worker-0. Запрос получается узлом с внутренним IP-адресом 10.240.0.20 (публичным NAT'ом занимается облачный провайдер), однако сервис в действительно запущен на другом узле (worker-1, что можно увидеть по IP-адресу endpoint'а — 10.200.1.18):
ubuntu@worker-0:~$ sudo conntrack -L | grep 31088
tcp 6 86397 ESTABLISHED src=173.38.XXX.XXX dst=10.240.0.20 sport=30303 dport=31088 src=10.200.1.18 dst=10.240.0.20 sport=80 dport=30303 [ASSURED] mark=0 use=1
Пакет пересылается от worker-0 к worker-1, где находит своего получателя:
ubuntu@worker-1:~$ sudo conntrack -L | grep 80
tcp 6 86392 ESTABLISHED src=10.240.0.20 dst=10.200.1.18 sport=14802 dport=80 src=10.200.1.18 dst=10.240.0.20 sport=80 dport=14802 [ASSURED] mark=0 use=1
Идеальна ли такая схема? Возможно, нет, но она работает. В данном случае запрограммированные правила iptables
таковы:
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/nginx:" -m tcp --dport 31088 -j KUBE-SVC-4N57TFCL4MD7ZTDA
-A KUBE-SVC-4N57TFCL4MD7ZTDA -m comment --comment "default/nginx:" -j KUBE-SEP-UGTFMET44DQG7H7H
-A KUBE-SEP-UGTFMET44DQG7H7H -p tcp -m comment --comment "default/nginx:" -m tcp -j DNAT --to-destination 10.200.1.18:80
Другими словами, адрес для получателя пакетов с портом 31088 транслируется в 10.200.1.18. Порт тоже транслируется, с 31088 на 80.
Мы не затронули другой тип сервисов — LoadBalancer
, — который делает сервис публично доступным с помощью балансировщика нагрузки облачного провайдера, но статья уже и без того получилась большой.
Могло показаться, что здесь множество информации, однако мы лишь затронули верхушку айсберга. В будущем я собираюсь рассказать про IPv6, IPVS, eBPF и пару интересных актуальных плагинов CNI.
Читайте также в нашем блоге:
Автор: Wimbo
Источник [51]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/seti/290729
Ссылки в тексте:
[1] Kubernetes The Hard Way: https://github.com/kelseyhightower/kubernetes-the-hard-way
[2] даже на AWS!: https://github.com/nleiva/kubernetes-the-hard-way
[3] CNI: https://github.com/containernetworking/cni/blob/master/SPEC.md
[4] попросту не существует: https://www.youtube.com/watch?v=t98CX8Tberc
[5] здесь: https://github.com/nleiva/kubernetes-networking-links
[6] контейнеров Nabla: https://nabla-containers.github.io/
[7] RFC 1918: https://tools.ietf.org/html/rfc1918
[8] GCP: https://cloud.google.com/sdk/gcloud/reference/compute/instances/create
[9] AWS: https://docs.aws.amazon.com/cli/latest/reference/ec2/run-instances.html
[10] controllers_gcp.sh
: https://gist.github.com/nleiva/6fa403a643b665c0bb36f4dc9744ba1e#file-controllers_gcp-sh
[11] controllers_aws.sh
: https://gist.github.com/nleiva/4a57c206899659c10348fdf14e84e07d#file-controllers_aws-sh
[12] политики безопасности верны: https://github.com/nleiva/kubernetes-the-hard-way/blob/master/docs/03-compute-resources.md#firewall-rules
[13] сетевую модель Kubernetes: https://kubernetes.io/docs/concepts/cluster-administration/networking/#kubernetes-model
[14] плагину CNI: https://github.com/containernetworking/cni/blob/master/SPEC.md#overview-1
[15] Container Network Interface Specification: https://github.com/containernetworking/cni/blob/master/SPEC.md#cni-plugin
[16] из man-страницы namespaces: http://man7.org/linux/man-pages/man7/namespaces.7.html
[17] Namespaces in operation: https://lwn.net/Articles/531114/
[18] man-страницы network namespaces: http://man7.org/linux/man-pages/man7/network_namespaces.7.html
[19] сетевые плагины: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/
[20] почему не CNM?: https://kubernetes.io/blog/2016/01/why-kubernetes-doesnt-use-libnetwork/
[21] Kubelet: https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/#kubelet
[22] Network Plugin Requirements: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#network-plugin-requirements
[23] kubelet.service
: https://github.com/nleiva/kubernetes-the-hard-way/blob/master/docs/09-bootstrapping-kubernetes-workers.md#configure-the-kubelet
[24] The Almighty Pause Container: https://www.ianlewis.org/en/almighty-pause-container
[25] конфиг для CNI: https://github.com/nleiva/kubernetes-the-hard-way/blob/master/docs/09-bootstrapping-kubernetes-workers.md#configure-cni-networking
[26] имя по умолчанию: https://github.com/containernetworking/plugins/blob/v0.6.0/plugins/main/bridge/bridge.go#L38
[27] плагин IPAM: https://github.com/containernetworking/plugins/tree/master/plugins/ipam
[28] описания host-local
: https://github.com/containernetworking/plugins/tree/master/plugins/ipam/host-local#overview
[29] берётся из подсети: https://github.com/containernetworking/plugins/blob/v0.6.0/pkg/ipam/ipam.go#L83-L89
[30] будет за NAT'ом: https://github.com/containernetworking/plugins/blob/v0.6.0/pkg/ip/ipmasq.go#L26-L74
[31] Image: https://twitter.com/miekg/status/1011585886654550016/photo/1
[32] отсюда: https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/13-smoke-test.md#deployments
[33] Docker: https://www.docker.com/
[34] containerd: https://github.com/containerd/containerd
[35] Интеграция containerd с Kubernetes, заменяющая Docker, готова к production: https://habr.com/company/flant/blog/414875/
[36] есть: https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Route_Tables.html#RouteTables
[37] обычно имеет второй адрес: https://cloud.google.com/vpc/docs/vpc#reserved_ip_addresses_in_every_subnet
[38] вручную: https://github.com/nleiva/kubernetes-the-hard-way/blob/master/docs/11-pod-network-routes.md#routes
[39] AWS CNI-plugin: https://github.com/aws/amazon-vpc-cni-k8s
[40] множество плагинов CNI: https://kubernetes.io/docs/concepts/cluster-administration/networking/#how-to-implement-the-kubernetes-networking-model
[41] busybox.yaml
: https://gist.github.com/nleiva/ed5be3164df3429652566f26717ce22f#file-busybox-yaml
[42] Kubernetes Services: https://kubernetes.io/docs/tutorials/kubernetes-basics/expose/expose-intro/
[43] kube-dns.yaml
: https://gist.github.com/nleiva/632e88a7639868dc6975624b95986821#file-kube-dns-yaml
[44] Smoke Test: Services: https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/13-smoke-test.md#services
[45] Иллюстрированное руководство по устройству сети в Kubernetes: https://habr.com/company/flant/blog/346304/
[46] Эксперименты с kube-proxy и недоступностью узла в Kubernetes: https://habr.com/company/flant/blog/359120/
[47] Улучшая надёжность Kubernetes: как быстрее замечать, что нода упала: https://habr.com/company/flant/blog/326062/
[48] Play with Kubernetes — сервис для практического знакомства с K8s: https://habr.com/company/flant/blog/415381/
[49] Наш опыт с Kubernetes в небольших проектах: https://habr.com/company/flant/blog/331188/
[50] Container Networking Interface (CNI) — сетевой интерфейс и стандарт для Linux-контейнеров: https://habr.com/company/flant/blog/329830/
[51] Источник: https://habr.com/post/420813/?utm_campaign=420813
Нажмите здесь для печати.