- PVSM.RU - https://www.pvsm.ru -

За кулисами сети в Kubernetes

Прим. перев.: Автор оригинальной статьи — Nicolas Leiva — архитектор решений Cisco, который решил поделиться со своими коллегами, сетевыми инженерами, о том, как устроена сеть Kubernetes изнутри. Для этого он исследует простейшую её конфигурацию в кластере, активно применяя здравый смысл, свои познания о сетях и стандартные утилиты Linux/Kubernetes. Получилось объёмно, зато весьма наглядно.

За кулисами сети в Kubernetes - 1

Помимо того факта, что руководство 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]).

А начнём мы с конца, когда у нас есть три контроллера и три рабочих узла:

За кулисами сети в Kubernetes - 2

Вы можете заметить, что здесь также есть как минимум три частные подсети! Немного терпения, и все они будут рассмотрены. Помните, что, хоть мы и ссылаемся на очень специфичные IP-префиксы, они попросту взяты из Kubernetes The Hard Way [1], так что имеют лишь локальную значимость, а вы вольны выбрать для своего окружения любой другой блок адресов в соответствии с RFC 1918 [7]. Для случая с IPv6 будет отдельная статья в блоге.

Сеть узла (10.240.0.0/24)

Это внутренняя сеть, частью которой являются все узлы. Определяется флагом --private-network-ip в GCP [8] или опцией --private-ip-address в AWS [9] при выделении вычислительных ресурсов.

Инициализация узлов контроллера в GCP

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])

Инициализация узлов контроллера в AWS

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])

За кулисами сети в Kubernetes - 3

Каждый экземпляр будет иметь два IP-адреса: частный от сети узла (контроллеры — 10.240.0.1${i}/24, воркеры — 10.240.0.2${i}/24) и публичный, назначенный облачным провайдером, о котором мы поговорим попозже, как доберёмся до NodePorts.

GCP

$ 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

$ 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 установлен на хост).

Сеть подов (10.200.0.0/16)

Это сеть, в которой живут поды. Каждый рабочий узел использует подсеть этой сети. В нашем случае — POD_CIDR=10.200.${i}.0/24 для worker-${i}.

За кулисами сети в Kubernetes - 4

Чтобы понять, как всё настроено, сделаем шаг назад и посмотрим на сетевую модель Kubernetes [13], которая требует следующего:

  • Все контейнеры могут общаться с любыми другими контейнерами без использования NAT.
  • Все узлы могут общаться со всеми контейнерами (и наоборот) без использования NAT.
  • IP, который видит контейнер, должен быть таким же, как его видят другие.

Реализация всего этого возможна разными способами, и Kubernetes передаёт сетевую настройку плагину CNI [14].

«Плагин CNI отвечает за добавление сетевого интерфейса в сетевом пространстве имён контейнера (например, один конец veth-пары) и выполнение необходимых изменений на хосте (например, подключение второго конца veth к мосту). Затем он должен назначить IP интерфейсу и настроить маршруты соответственно разделу «IP Address Management» путём вызова нужного плагина IPAM». (из Container Network Interface Specification [15])

За кулисами сети в Kubernetes - 5

Сетевое пространство имён

«Пространство имён оборачивает глобальный системный ресурс в абстракцию, которая видна процессам в этом пространстве имён таким образом, что у них есть свой изолированный экземпляр глобального ресурса. Изменения в глобальном ресурсе видны другим процессам, входящим в это пространство имён, но не видны иным процессам». (из man-страницы namespaces [16])

Linux предоставляет семь различных пространств имён (Cgroup, IPC, Network, Mount, PID, User, UTS). Сетевые (Network) пространства имён (CLONE_NEWNET) определяют сетевые ресурсы, которые доступны процессу: «У каждого сетевого пространства имён есть свои сетевые устройства, IP-адреса, таблицы IP-маршрутизации, директория /proc/net, номера портов и так далее» (из статьи «Namespaces in operation [17]»).

Виртуальные Ethernet-устройства (Veth)

«Виртуальная сетевая пара (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).

За кулисами сети в Kubernetes - 6

Также будет настроена veth-пара для подключения пода к только что созданному мосту:

За кулисами сети в Kubernetes - 7

Для назначения 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 не нужен…

За кулисами сети в Kubernetes - 8 [31]

Маршрутизация пода

Теперь мы готовы настраивать поды. Посмотрим на все сетевые пространства имён одного из рабочих узлов и проанализируем одно из них после создания 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):

  • nginx (ID: 4c0bd2e2e5c0b17c637af83376879c38f2fb11852921b12413c54ba49d6983c7);
  • pause (ID: d19b1b1c92f7cc90764d4f385e8935d121bca66ba8982bae65baff1bc2841da6).

За кулисами сети в Kubernetes - 9

Как же этот под (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

Итак, картина выглядит следующим образом:

За кулисами сети в Kubernetes - 10

Проверка маршрутизации

Как мы на самом деле пересылаем трафик? Посмотрим на таблицу маршрутизации в сетевом пространстве имён пода:

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], а мы рассматриваем пример простейшей сетевой конфигурации:

За кулисами сети в Kubernetes - 11

Глубокое погружение в NAT

Командой 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

Сеть кластера (10.32.0.0/24)

Возможно, вы заметили в примере с 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.

P.S. от переводчика

Читайте также в нашем блоге:

Автор: 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