В чем силиум, брат? Обзор ключевых фишек Cilium и его преимущества на фоне других CNI-проектов

от автора

Привет! Меня зовут Даниил, я DevOps-инженер в KTS.

Сегодня расскажу о Cilium – опенсорсном CNI-плагине для Kubernetes с технологией eBPF под капотом.

Помимо CNI, Cilium предоставляет множество фич, которые также используют eBPF и в совокупности покрывают почти весь нетворкинг в Kubernetes. Их я и рассмотрю в этой статье, попутно описав свои впечатления и трудности, с которыми пришлось столкнуться.

Тестировать я буду два кластера: первый – обычный, развернутый через kubeadm, а второй – managed k8s в Yandex Cloud. Для проведения испытаний я буду использовать последнюю на момент написания статьи версию Cilium – 1.15.1.

Оглавление

В чем фишка eBPF?

Технологию eBPF можно смело назвать киллер-фичей Cilium. Какую же задачу она выполняет?

Чтобы ответить на этот вопрос, вспомним, что все действия и события в операционной системе так или иначе проходят через ядро. Следовательно, это наилучшее место для реализации функционала, связанного с сетями, безопасностью и наблюдаемостью. Однако разработка такого проекта может занять много времени или вовсе быть отклонена сообществом, а поддержка собственной версии ядра будет слишком дорогостоящей и неудобной для многих компаний.

Эту проблему и решает технология eBPF. Она предоставляет платформу для простого и безопасного запуска программ в ядре. По словам Томаса Графа, одного из основателей Cilium, «eBPF для ядра Linux – как JavaScript для браузера».

Установка

Перед тем, как перейти к описанию фич и тестов, поговорим о том, как установить Cilium.

  • Если вы не используете управляемый облачным провайдером Kubernetes-сервис, где нужный плагин идет из коробки, то вам придется накатить его вручную.

    Установить Cilium можно с помощью CLI-утилиты cilium, которая установит Helm-чарт (либо просто установить чарт напрямую через Helm):

      $ cilium install --version 1.15.1 ℹ  Using Cilium version 1.15.1 🔮 Auto-detected cluster name: kubernetes 🔮 Auto-detected kube-proxy has been installed

    С помощью helm list -n kube-system можно убедиться, что чарт установлен:

    $ helm list -n kube-system NAME    NAMESPACE       REVISION        UPDATED                                 STATUS          CHART           APP VERSION cilium  kube-system     1               2024-02-19 14:24:38.564620737 +0000 UTC deployed        cilium-1.15.1   1.15.1 

    Через cilium status можно посмотреть состояние Cilium. Если вы видите что-то подобное, это значит, что Cilium работает исправно:

        /¯¯\\  /¯¯\\__/¯¯\\    Cilium:             OK  \\__/¯¯\\__/    Operator:           OK  /¯¯\\__/¯¯\\    Envoy DaemonSet:    disabled (using embedded mode)  \\__/¯¯\\__/    Hubble Relay:       disabled     \\__/       ClusterMesh:        disabled  Deployment             cilium-operator    Desired: 1, Ready: 1/1, Available: 1/1 DaemonSet              cilium             Desired: 3, Ready: 3/3, Available: 3/3 Containers:            cilium             Running: 3                        cilium-operator    Running: 1 Cluster Pods:          2/2 managed by Cilium Helm chart version:    1.15.1 Image versions         cilium             quay.io/cilium/cilium:v1.15.1@sha256:351d6685dc6f6ffbcd5451043167cfa8842c6decf80d8c8e426a417c73fb56d4: 3                        cilium-operator    quay.io/cilium/operator-generic:v1.15.1@sha256:819c7281f5a4f25ee1ce2ec4c76b6fbc69a660c68b7825e9580b1813833fa743: 1
  • Если же вы управляете кластером через Yandex Cloud, то вам достаточно просто отметить чекбокс «Включить туннельный режим» при создании кластера:

Поговорим о производительности

После установки Cilium можно переходить описанию фишек и их тестам. В первую очередь я расскажу о том, какими способами и насколько значительно плагин повышает производительность нагруженных сервисами кластеров.

Пока, kube-proxy

Одна из уникальных фич Cilium – возможность полностью заменить kube-proxy. Зачем это может понадобиться? Затем, чтобы повысить производительность больших кластеров с большим количеством сервисов. Для этого при инициализации кластера нужно пропустить установку kube-proxy:

$ kubeadm init --skip-phases=addon/kube-proxy
  • Если устанавливать Cilium через Helm, то в values чарта необходимо указать следующие параметры:

    kubeProxyReplacement: true k8sServiceHost: <адрес kube-apiserver> k8sServicePort: <порт kube-apiserver>
  • Если же выполнять установку с помощью утилиты cilium, то CLI автоматически определит отсутствие kube-proxy и подставит нужные параметры:

    $ cilium install --version 1.15.1 ℹ  Using Cilium version 1.15.1 🔮 Auto-detected cluster name: kubernetes 🔮 Auto-detected kube-proxy has not been installed ℹ  Cilium will fully replace all functionalities of kube-proxy

Статус замены можно проверить в поде Cilium-агента:

$ kubectl exec -n kube-system cilium-pnjxh -- cilium status --verbose <...> KubeProxyReplacement Details:   Status:                 Strict   Socket LB:              Enabled   Socket LB Tracing:      Enabled   Socket LB Coverage:     Full   Devices:                eth0   10.12.12.5 fe80::d20d:10ff:fe08:9a52 (Direct Routing)   Mode:                   SNAT   Backend Selection:      Random   Session Affinity:       Enabled   Graceful Termination:   Enabled   NAT46/64 Support:       Disabled   XDP Acceleration:       Disabled   Services:   - ClusterIP:      Enabled   - NodePort:       Enabled (Range: 30000-32767)   - LoadBalancer:   Enabled   - externalIPs:    Enabled   - HostPort:       Enabled <...>

Если параметру «Status» соответствует значение «Strict» или «True», это значит, что агент запущен в режиме замены kube-proxy.

Тесты производительности

На выступлении «Liberating Kubernetes From Kube-proxy and Iptables» Мартинас Пумпутис, один из разработчиков Cilium, показал результаты бенчмарков, где сравнивается производительность Cilium (eBPF) и kube-proxy (ipvs и iptables).

Ось Y на слайде – время задержки в микросекундах, ось X – количество сервисов в кластере.

Как видно, при увеличении числа сервисов растет задержка kube-proxy в режиме iptables. Это связано с тем, что для каждого сервиса kube-proxy создает правила в цепи iptables, который обрабатывает их последовательно (алгоритм сложности O(n)). Соответственно, чем больше правил в цепи, тем больше задержка сетевого пакета.

Задержка Cilium и kube-proxy в режиме ipvs почти не меняется, так как оба плагина используют алгоритм сложности O(1) (в Cilium это lookup hash-таблицы), при этом Cilium остается чуть быстрее. Для тестирования разработчики использовали netperf (конкретно – тест TCP_CRR), который замеряет следующую последовательность:

  1. открывается TCP-соединение;

  2. отправляется единичный запрос;

  3. приходит ответ;

  4. соединение закрывается.

Проведем похожий тест и сравним результаты. Для простоты исключим ipvs, будем сравнивать только eBPF и iptables. В качестве тестового стенда я использовал кластер из трех нод (одна мастер-нода и два воркера) на Ubuntu 22.04 в Yandex Cloud.

netperf состоит из клиентской и серверной частей, поэтому сначала деплоим netserver в кластер (для теста будем использовать NodePort сервис, как и разработчики):

Запуск netserver
apiVersion: apps/v1 kind: Deployment metadata:   name: netserver   labels:     app: netserver spec:   replicas: 1   selector:     matchLabels:       app: netserver   template:     metadata:       labels:         app: netserver     spec:       containers:       - name: netserver         image: networkstatic/netserver:latest         args:           - -D         ports:         - containerPort: 12865         - containerPort: 30002 --- apiVersion: v1 kind: Service metadata:   name: netserver spec:   type: NodePort   selector:     app: netserver   ports:     # netperf использует 2 соединения - одно для передачи информации о тесте, другое для самого теста     - protocol: TCP       port: 12865       targetPort: 12865       nodePort: 30001       name: control     - protocol: TCP       port: 30002       targetPort: 30002       nodePort: 30002       name: data

Проверяем по логам, что сервер запущен и работает:

$ kubectl logs <под netserver> Starting netserver with host 'IN(6)ADDR_ANY' port '12865' and family AF_UNSPEC

Теперь можно начать запускать тесты. Для запуска используем внекластерный хост в той же подсети, в которой находится кластер. В качестве целевого хоста выступит нода, на которой запущен под с netserver.

$ netperf -t TCP_CRR -H <адрес ноды кластера> -p 30001 -- -P 30002 -o rt_latency

Подробнее о флагах:

  • t TCP_CRR – тип теста, который нужно запустить;

  • H – хост, с которым нужно протестировать соединение (там должен быть запущен netserver);

  • p 30001 – порт, который прослушивает netserver.

После «—» указываем параметры для испытания. Разработчики в своем выступлении параметров не раскрывали, поэтому мы укажем только порт для проведения теста (-P 30002) и параметр, который следует выводить в результатах. Скорее всего, под «µseq per tx» на слайде разработчики имели в виду rt_latency, поэтому указываем его.

Далее запускаем скрипт для быстрого создания множества сервисов:

for i in {1..2767}; do cat <<EOF | kubectl delete -f - apiVersion: v1 kind: Service metadata:   name: netserver-$i spec:   selector:     app: netserver   ports:     - protocol: TCP       port: 12865       targetPort: 12865       name: control     - protocol: TCP       port: 30002       targetPort: 30002       name: data EOF done

У меня получился следующий результат:

Конечно, тест не претендует на предельную точность, но даже грубая оценка дает понять разницу между решениями – при использовании kube-proxy задержка растет с количеством сервисов, в то время как Cilium позволяет держать ее примерно на одном уровне.

Ключевые фичи Cilium

Итак, какой дополнительный функционал предоставляет плагин? Первое испытание уже показало нам, что eBPF позволяет сохранять скорость передачи пакетов данных даже при большом количестве развернутых сервисов. Однако на этом преимущества Cilium не заканчиваются, ведь благодаря нему можно значительно упростить работу и с большим количеством кластеров. Давайте разбираться, каким образом.

Сетевая безопасность и политики

Cilium позволяет гибко настраивать ограничения на сетевое взаимодействие в кластере с помощью сетевых политик. Их применение в Cilium похоже на применение стандартных сетевых политик Kubernetes: по умолчанию у пода нет никаких ограничений на ingress- и egress-трафик, но если он попадает под селектор хотя бы одной политики, то переходит в режим «default deny». Таким образом, весь трафик, кроме того, что разрешен политиками, блокируется. Ответный трафик имплицитно разрешен. Cilium также предоставляет еще два режима:

  • always – в этом режиме правило «default deny» распространяется на все поды независимо от того, попадают они под селектор сетевых политик или нет;

  • never – в этом режиме применение сетевых политик отключено, трафик в кластере передается без ограничений.

Указать режим применения сетевых политик можно в values.yaml следующим образом:

extraEnv: - name: CILIUM_ENABLE_POLICY   value: <default,always,never>

Чтобы понять, как использовать сетевые политики Cilium, рассмотрим следующий пример. Допустим, у нас есть проект, состоящий из двух сервисов (backend1 и backend2) и базы данных. backend1 в процессе своей работы обращается backend2, а backend2 обращается к БД. Тогда манифест сетевой политики CiliumNetworkPolicy для сервиса backend2 может выглядеть следующим образом:

apiVersion: "cilium.io/v2" kind: CiliumNetworkPolicy metadata:   name: example spec:   endpointSelector:     matchLabels:       app: backend2   ingress:   - fromEndpoints:     - matchLabels:         app: backend1     toPorts:     - ports:       - port: "5000"         protocol: TCP   egress:   - toEndpoints:     - matchLabels:         app: database     toPorts:     - ports:       - port: "5432"         protocol: TCP   description: Allow backend1 ingress and DB egress

Здесь можно выделить основные поля:

  • endpointSelector – селектор для подов, на которые будет распространяться политика;

  • ingress – правила для входящего трафика;

  • egress – правила для исходящего трафика;

  • description – описание политики (полезно, когда забудете, для чего вы вообще ее добавляли).

Как видно, в данном манифесте политика распространяется на поды с лейблом app: backend2 и разрешает входящий трафик из подов с лейблом app: backend1 на порт 5000 и исходящий трафик в поды с лейблом app: database на порт 5432. 

Разумеется, настроить таким образом сетевые правила можно и через Calico, и даже с помощью стандартных политик Kubernetes, однако возможности политик Cilium этим не ограничиваются.

Правила ingress и egress

Фильтрация трафика в Cilium основана на «идентичностях» (identity). Идентичность представляет один или несколько эндпоинтов в сети и технически является числом, связанным с набором лейблов. В случае с подами, лейблы создаются на основе метаданных подов.

Через Cilium-агента можно посмотреть список идентичностей в кластере:

Пример списка идентичностей
$ kubectl exec -n kube-system ds/cilium -- cilium identity list  ID      LABELS 1       reserved:host 2       reserved:world 3       reserved:unmanaged 4       reserved:health 5       reserved:init 6       reserved:remote-node 7       reserved:kube-apiserver         reserved:remote-node 8       reserved:ingress 9       reserved:world-ipv4 10      reserved:world-ipv6 6488    k8s:app=backend1         k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=default         k8s:io.cilium.k8s.policy.cluster=kubernetes         k8s:io.cilium.k8s.policy.serviceaccount=default         k8s:io.kubernetes.pod.namespace=default 13657   k8s:app=database         k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=default         k8s:io.cilium.k8s.policy.cluster=kubernetes         k8s:io.cilium.k8s.policy.serviceaccount=default         k8s:io.kubernetes.pod.namespace=default 24168   k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=kube-system         k8s:io.cilium.k8s.policy.cluster=kubernetes         k8s:io.cilium.k8s.policy.serviceaccount=coredns         k8s:io.kubernetes.pod.namespace=kube-system         k8s:k8s-app=kube-dns 64056   k8s:app=backend2         k8s:io.cilium.k8s.namespace.labels.kubernetes.io/metadata.name=default         k8s:io.cilium.k8s.policy.cluster=kubernetes         k8s:io.cilium.k8s.policy.serviceaccount=default         k8s:io.kubernetes.pod.namespace=default

Сетевая политика в примере выше разрешает трафик из идентичности 6488 (k8s:app=backend1) в идентичность 64056 (k8s:app=backend2) и из идентичности 64056 в идентичность 13657 (k8s:app=database). Идентичности записываются в каждый сетевой пакет, который перемещается между нодами кластера, что позволяет Cilium эффективно фильтровать трафик по правилам сетевых политик.

В качестве целей правил ingress и egress можно также использовать:

  • CIDR:

    spec:   egress:   - toCIDR:     - 192.168.3.1/32
  • сервисы:

    spec:   egress:   - toServices:     - k8sService:         serviceName: some-service         namespace: default
  • DNS-имена:

    spec:   egress:   - toFQDNs:     - matchName: "example.com"
  • ICMP:

    spec:   egress:   - icmps:     - fields:       - type: 0         family: IPv4
  • «сущности» (entity):

    spec:   egress:   - toEntities:     - host

Под сущностями подразумеваются следующие категории эндпоинтов:

  • host – хост, на котором запущен под;

  • remote-node – все ноды в кластере, кроме той, на которой запущен под;

  • kube-apiserver – внутренний и внешний эндпоинты kube-apiserver;

  • ingress – инстанс Envoy, который Cilium использует для ingress-трафика;

  • cluster – все эндпоинты кластера, включая ноды и поды, которые не управляются Cilium;

  • init – эндпоинты, для которых Cilium пока не установил идентичность;

  • health – health-эндпоинты нод;

  • unmanaged – поды, которые не управляются Cilium;

  • world – внекластерные эндпоинты;

  • all – думаю, и так понятно.

Внимательный читатель заметит, что этот список совпадает с первыми десятью идентичностями, зарезервированными Cilium.

L7

Еще одна фича сетевых политик Cilium – возможность применять сетевые политики для нескольких протоколов на уровне L7 (HTTP и Kafka). Для этого он использует инстанс Envoy, развернутый в качестве DaemonSet или вшитый в Cilium-агент.

Чтобы понять, как этим пользоваться, вернемся еще раз к примеру с двумя сервисами и БД. Допустим, нам нужно, чтобы backend1 мог отправлять только GET запросы на путь /api/users в backend2. Тогда наш манифест будет выглядеть так:

apiVersion: "cilium.io/v2" kind: CiliumNetworkPolicy metadata:   name: example spec:   endpointSelector:     matchLabels:       app: backend2   ingress:   - fromEndpoints:     - matchLabels:         env: backend1     toPorts:     - ports:       - port: "5000"         protocol: TCP       rules:         http:         - method: "GET"           path: "/api/users"

Сетевые политики нод

Мы также можем использовать сетевые политики в качестве фаервола для хостов, которые находятся под управлением Cilium. Для этого в values.yaml нужно добавить hostFirewall.enabled: true. В поле devices: можно указать нужные сетевые интерфейсы (если не указывать, то Cilium определит их автоматически).

Обратите внимание: с хостовыми политиками нужно быть осторожным, так как можно нарушить работу нод, заблокировав, например, сетевой доступ к kube-apiserver. Эти политики не влияют на коммуникацию между подами (если они не используют hostNetwork: true) и их нельзя использовать для правил уровня L7.

Например, для того, чтобы разрешить доступ к нодам кластера по SSH, можно использовать следующий манифест:

apiVersion: "cilium.io/v2" kind: CiliumClusterwideNetworkPolicy metadata:   name: example spec:   nodeSelector:     matchLabels:       node: ssh   ingress:   - fromEntities:     - cluster   - toPorts:     - ports:       - port: "22"         protocol: TCP

Как видно, здесь используется тип CiliumClusterwideNetworkPolicy, правила которого распространяются на весь кластер, а не на отдельный неймспейс.

Далее вместо endpointSelector нужно указать nodeSelector с лейблами нод, на которые будет распространяться политика (node: ssh). В правилах мы разрешаем доступ из сущности cluster, чтобы не нарушить коммуникации с другими нодами кластера, и доступ к порту 22.

Визуальный редактор политик

Еще стоит упомянуть про редактор от разработчиков Cilium, в котором с помощью визуальных блоков можно получить манифест NetworkPolicy или CiliumNetworkPolicy. Попробовать можно здесь.

Observability и Hubble

Hubble – это решение для мониторинга сетевого трафика, которое поставляется вместе с Cilium. Благодаря eBPF мониторинг происходит прямо в ядре Linux, без необходимости в sidecar-контейнерах и т.п. Hubble состоит из двух компонентов:

  1. Hubble-сервер, вшитый в Cilium-агент. Он отвечает за мониторинг трафика ноды, на которой он развернут.

  2. Hubble Relay, который объединяет события всех Hubble-серверов и предоставляет общекластерный API.

Для того, чтобы включить фичи Hubble, в values.yaml необходимо добавить следующее:

hubble-ui.yaml
--- # Source: cilium/templates/hubble-ui-serviceaccount.yaml apiVersion: v1 kind: ServiceAccount metadata:   name: "hubble-ui"   namespace: kube-system --- # Source: cilium/templates/hubble-ui-configmap.yaml apiVersion: v1 kind: ConfigMap metadata:   name: hubble-ui-envoy   namespace: kube-system data:   envoy.yaml: |     static_resources:       listeners:         - name: listener_hubble_ui           address:             socket_address:               address: 0.0.0.0               port_value: 8081           filter_chains:             - filters:                 - name: envoy.filters.network.http_connection_manager                   typed_config:                     "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager                     codec_type: auto                     stat_prefix: ingress_http                     route_config:                       name: local_route                       virtual_hosts:                         - name: local_service                           domains: ["*"]                           routes:                             - match:                                 prefix: "/api/"                               route:                                 cluster: backend                                 prefix_rewrite: "/"                                 timeout: 0s                                 max_stream_duration:                                   grpc_timeout_header_max: 0s                             - match:                                 prefix: "/"                               route:                                 cluster: frontend                           cors:                             allow_origin_string_match:                               - prefix: "*"                             allow_methods: GET, PUT, DELETE, POST, OPTIONS                             allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout                             max_age: "1728000"                             expose_headers: grpc-status,grpc-message                     http_filters:                       - name: envoy.filters.http.grpc_web                       - name: envoy.filters.http.cors                       - name: envoy.filters.http.router       clusters:         - name: frontend           connect_timeout: 0.25s           type: strict_dns           lb_policy: round_robin           load_assignment:             cluster_name: frontend             endpoints:               - lb_endpoints:                   - endpoint:                       address:                         socket_address:                           address: 127.0.0.1                           port_value: 8080         - name: backend           connect_timeout: 0.25s           type: logical_dns           lb_policy: round_robin           http2_protocol_options: {}           load_assignment:             cluster_name: backend             endpoints:               - lb_endpoints:                   - endpoint:                       address:                         socket_address:                           address: 127.0.0.1                           port_value: 8090 --- # Source: cilium/templates/hubble-ui-clusterrole.yaml kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata:   name: hubble-ui rules:   - apiGroups:       - networking.k8s.io     resources:       - networkpolicies     verbs:       - get       - list       - watch   - apiGroups:       - ""     resources:       - componentstatuses       - endpoints       - namespaces       - nodes       - pods       - services     verbs:       - get       - list       - watch   - apiGroups:       - apiextensions.k8s.io     resources:       - customresourcedefinitions     verbs:       - get       - list       - watch   - apiGroups:       - cilium.io     resources:       - "*"     verbs:       - get       - list       - watch --- # Source: cilium/templates/hubble-ui-clusterrolebinding.yaml kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata:   name: hubble-ui roleRef:   apiGroup: rbac.authorization.k8s.io   kind: ClusterRole   name: hubble-ui subjects:   - kind: ServiceAccount     namespace: kube-system     name: "hubble-ui" --- # Source: cilium/templates/hubble-ui-service.yaml kind: Service apiVersion: v1 metadata:   name: hubble-ui   labels:     k8s-app: hubble-ui   namespace: kube-system spec:   selector:     k8s-app: hubble-ui   ports:     - name: http       port: 80       targetPort: 8081   type: ClusterIP --- # Source: cilium/templates/hubble-ui-deployment.yaml kind: Deployment apiVersion: apps/v1 metadata:   namespace: kube-system   labels:     k8s-app: hubble-ui   name: hubble-ui spec:   replicas: 1   selector:     matchLabels:       k8s-app: hubble-ui   template:     metadata:       annotations:       labels:         k8s-app: hubble-ui     spec:       securityContext:         runAsUser: 1001       serviceAccount: "hubble-ui"       serviceAccountName: "hubble-ui"       containers:         - name: frontend           image: "quay.io/cilium/hubble-ui:v0.7.9@sha256:e0e461c680ccd083ac24fe4f9e19e675422485f04d8720635ec41f2ba9e5562c"           imagePullPolicy: IfNotPresent           ports:             - containerPort: 8080               name: http           resources: {}         - name: backend           image: "quay.io/cilium/hubble-ui-backend:v0.7.9@sha256:632c938ef6ff30e3a080c59b734afb1fb7493689275443faa1435f7141aabe76"           imagePullPolicy: IfNotPresent           env:             - name: EVENTS_SERVER_PORT               value: "8090"             - name: FLOWS_API_ADDR               value: "hubble-relay:80"           ports:             - containerPort: 8090               name: grpc           resources: {}         - name: proxy           image: "docker.io/envoyproxy/envoy:v1.18.2@sha256:e8b37c1d75787dd1e712ff389b0d37337dc8a174a63bed9c34ba73359dc67da7"           imagePullPolicy: IfNotPresent           ports:             - containerPort: 8081               name: http           resources: {}           command: ["envoy"]           args: ["-c", "/etc/envoy.yaml", "-l", "info"]           volumeMounts:             - name: hubble-ui-envoy-yaml               mountPath: /etc/envoy.yaml               subPath: envoy.yaml       volumes:         - name: hubble-ui-envoy-yaml           configMap:             name: hubble-ui-envoy

Здесь помимо Hubble Relay включается Hubble UI – веб GUI-клиент. Командой cilium hubble ui можно создать для него port-forward. Запустим демо из репозитория Cilium и отправим запрос из одного пода в другой:

kubectl create -f <https://raw.githubusercontent.com/cilium/cilium/HEAD/examples/kubernetes-grpc/cc-door-app.yaml> kubectl exec terminal-87 -- python3 /cloudcity/cc_door_client.py GetName 1

Демо состоит из пода terminal-87, деплоймента cc-door-mgr и его сервиса. Второй командой мы отправим gRPC-запрос в cc-door-mgr. Если в это время открыть Hubble UI в браузере, то он в реальном времени нарисует карту сетевого взаимодействия:

В данном случае отображены две идентичности – public-terminal, которая относится к поду terminal-87, и cc-door-mgr, которая относится к подам деплоймента cc-door-mgr. Стрелка между ними указывает направление трафика: в данном случае трафик шел от public-terminal до cc-door-mgr на порт 50051 по протоколу TCP. 

Ниже можно увидеть список пакетов в составе трафика, в котором можно посмотреть дополнительную информацию, в том числе вердикт сетевых политик (т.е. был ли заблокирован этот пакет сетевой политикой или нет), TCP-флаги, лейблы идентичностей и т.д.

Также у Hubble есть CLI-клиент. В поде Cilium-агента можно получить примерно ту же информацию о трафике, если выполнить команду hubble observe:

$ hubble observe -n default  Apr  3 07:38:36.239: default/terminal-87:52380 (ID:6040) -> default/cc-door-mgr-76658457d4-sm5dp:50051 (ID:1419) to-endpoint FORWARDED  (TCP Flags: ACK, PSH) Apr  3 07:38:36.239: default/terminal-87:52380 (ID:6040) <> default/cc-door-mgr-76658457d4-sm5dp (ID:1419) pre-xlate-rev TRACED (TCP) Apr  3 07:38:36.239: default/terminal-87:52380 (ID:6040) <- default/cc-door-mgr-76658457d4-sm5dp:50051 (ID:1419) to-endpoint FORWARDED  (TCP Flags: ACK, PSH) Apr  3 07:38:36.240: default/terminal-87:52380 (ID:6040) <- default/cc-door-mgr-76658457d4-sm5dp:50051 (ID:1419) to-endpoint FORWARDED  (TCP Flags: ACK, FIN) Apr  3 07:38:36.240: default/terminal-87:52380 (ID:6040) -> default/cc-door-mgr-76658457d4-sm5dp:50051 (ID:1419) to-endpoint FORWARDED  (TCP Flags: ACK) Apr  3 07:38:36.240: default/terminal-87:52380 (ID:6040) -> default/cc-door-mgr-76658457d4-sm5dp:50051 (ID:1419) to-endpoint FORWARDED  (TCP Flags: ACK, FIN) <...>

Наконец, у Hubble есть эндпоинт с метриками для построения графиков и дэшбордов. Чтобы его включить, в values.yaml указываются нужные метрики. С полным списком метрик можно ознакомиться здесь.

hubble:   metrics:     enabled: "{tcp,icmp,httpV2,drop}"

Мониторинг L7

На скриншотах Hubble выше вы могли заметить, что в таблице с сетевыми пакетами есть пустой столбец «L7 info». Почему он пуст, если мы отправляем gRPC-запрос, т.е. запрос на уровне L7? Дело в том, что Cilium и Hubble по умолчанию предоставляют мониторинг только на уровнях L3/L4. Для того, чтобы события уровня L7 стали видны, нужно создать сетевую политику с правилом уровня L7. В контексте демо мы можем использовать следующий манифест:

apiVersion: "cilium.io/v2" kind: CiliumNetworkPolicy metadata:   name: example spec:   endpointSelector:     matchLabels:       app: cc-door-mgr   ingress:   - fromEndpoints:     - matchLabels:         app: public-terminal     toPorts:     - ports:       - port: "50051"         protocol: TCP       rules:         http:         - method: "POST"           path: "/cloudcity.DoorManager/GetName"

Так как gRPC работает поверх HTTP, то мы можем использовать HTTP-правило с путем, который соответствует gRPC-вызову. В данном случае это /cloudcity.DoorManager/GetName. Теперь любой L7-трафик, который проходит через эту политику, будет виден в Hubble:

Немного более подробную информацию можно увидеть через CLI (например, код ответа и задержку):

$ hubble observe -n default -f -t l7  Apr  3 17:19:45.115: default/terminal-87:41982 (ID:6040) -> default/cc-door-mgr-76658457d4-sm5dp:50051 (ID:1419) http-request FORWARDED (HTTP/2 POST <http://cc-door-server:50051/cloudcity.DoorManager/GetName>) Apr  3 17:19:45.117: default/terminal-87:41982 (ID:6040) <- default/cc-door-mgr-76658457d4-sm5dp:50051 (ID:1419) http-response FORWARDED (HTTP/2 200 1ms (POST <http://cc-door-server:50051/cloudcity.DoorManager/GetName>))

Мониторинг и применение сетевых политик на уровне L7 возможны благодаря инъекции Envoy-прокси в соединение, которое проходит через сетевую политику. «Redirected» в столбце «Verdict» на скриншоте выше как раз означает перенаправление трафика на инстанс Envoy.

L7-трафик можно просматривать даже в TLS-зашифрованных соединениях. Это реализовано с помощью сетевых политик, что позволяет расшифровывать трафик выборочно. Я бы не назвал эту фичу удобной, так как для этого нужно добавлять в под внутренний CA-сертификат и выпустить сертификаты для каждого хоста, соединение с которым вы хотите просматривать, однако в некоторых ситуациях это бывает полезно. Подробный пошаговый гайд об этом можно почитать здесь.

Ingress-контроллер и Gateway API

Cilium имеет свою реализацию ingress-контроллера и Gateway API. Для использования этих фич сначала включаем замену kube-proxy (см. раздел Пока, kube-proxy), а в values.yaml добавляем следующее:

ingressController:   enabled: true   loadbalancerMode: shared

Далее в поле ingressController.loadbalancerMode указываем режим использования балансировщиков:

  • dedicated – балансировщики создаются отдельно для каждого ingress;

  • shared – для всех ingress создается один общий балансировщик.

Если вы хотите использовать другой сервис для ingress-контроллера, например NodePort, то можете сконфигурировать его следующим образом:

ingressController:   enabled: true   loadbalancerMode: shared   service:     type: NodePort     insecureNodePort: 30080     secureNodePort: 30443

Здесь поля insecureNodePort и secureNodePort используются для указания портов HTTP и HTTPS соответственно, а в type указывается тип сервиса.

Затем устанавливаем соответствующие CRD для Gateway API:

$ kubectl apply -f <https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.0.0/config/crd/standard/gateway.networking.k8s.io_gatewayclasses.yaml> $ kubectl apply -f <https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.0.0/config/crd/standard/gateway.networking.k8s.io_gateways.yaml> $ kubectl apply -f <https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.0.0/config/crd/standard/gateway.networking.k8s.io_httproutes.yaml> $ kubectl apply -f <https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.0.0/config/crd/standard/gateway.networking.k8s.io_referencegrants.yaml> $ kubectl apply -f <https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.0.0/config/crd/experimental/gateway.networking.k8s.io_grpcroutes.yaml> $ kubectl apply -f <https://raw.githubusercontent.com/kubernetes-sigs/gateway-api/v1.0.0/config/crd/experimental/gateway.networking.k8s.io_tlsroutes.yaml>

И в values.yaml добавляем следующее:

gatewayAPI:   enabled: true

Также рекомендую после раскатки чарта перезапустить Cilium-агенты и оператор:

$ kubectl -n kube-system rollout restart deployment/cilium-operator $ kubectl -n kube-system rollout restart ds/cilium

В кластере должны появиться IngressClass и GatewayClass cilium.

Учтите, что Ingress- и Gateway API-контроллеры не будут работать, пока для них не появится хотя бы один Ingress- или Gateway API-ресурс соответственно. Также для работы ингресса в ядре должны быть загружены модули xt_socket и iptable_raw. В некоторых системах (например, NixOS) они могут не быть загружены по умолчанию.

В целом обе реализации имеют очень базовый функционал, не хватает продвинутых фич по типу авторизации (хотя бы basic auth), rate-лимитов и т.п. Поэтому, на мой взгляд, они пока не подходят для production-сред.

L4-балансировщики

Если вы эксплуатируете Kubernetes-кластер, который не находится под управлением облачного провайдера, то для доступа своих сервисов извне вы, скорее всего, будете использовать один из следующих вариантов:

  1. NodePort или hostNetwork;

  2. NodePort или hostNetwork с внешним балансировщиком;

  3. балансировщик Cloud native, работающий в кластере.

Балансировщик Cilium относится к 3 категории, и UX со стороны администратора кластера очень похож на MetalLB – еще одно популярное решение для балансировки.

Чтобы использовать Cilium для балансировки, сначала нужно указать диапазон адресов, которые будут использоваться для сервисов с типом LoadBalancer. Это можно сделать с помощью ресурса CiliumLoadBalancerIPPool:

apiVersion: "cilium.io/v2alpha1" kind: CiliumLoadBalancerIPPool metadata:   name: "pool" spec:   blocks:   - cidr: "192.168.1.0/24"   - start: "192.168.2.2"     stop: "192.168.2.30"   - start: "192.168.31.2"

В отличие от предыдущих фич, контроллер для этих объектов всегда активирован, но начинает работу после того, как в кластере появится хотя бы один ресурс CiliumLoadBalancerIPPool. В данном примере пул адресов распространяется на все сервисы, но мы можем их ограничить с помощью serviceSelector:

apiVersion: "cilium.io/v2alpha1" kind: CiliumLoadBalancerIPPool metadata:   name: "pool" spec:   blocks:   - cidr: "192.168.1.0/24"   - start: "192.168.2.2"     stop: "192.168.2.30"   - start: "192.168.31.2"   serviceSelector:     matchLabels:       app: backend

Обратите внимание: если создать пулы с пересекающимися диапазонами, то последний из них окажется конфликтующим и не будет выдавать адреса до тех пор, пока пересечение не будет устранено:

$ kubectl get ippool NAME      DISABLED   CONFLICTING   IPS AVAILABLE   AGE pool-1    false      False         30              2m pool-2    false      True          30              14s

После того, как манифест с пулом адресов был применен, у соответствующих сервисов с типом LoadBalancer должен появиться внешний адрес (status.loadBalancer.ingress). Далее нужно решить, каким образом об этих адресах узнает внешняя сеть. Тут Cilium предлагает два способа – BGP и L2 Announcement. В рамках статьи рассмотрим только L2 Announcement, так как его проще воспроизвести в тестовых условиях (например, в домашней сети).

L2 Announcement работает с помощью протокола ARP, который, по сути, является маппингом между IP и MAC-адресами. Это значит, что адреса сервисов не назначаются напрямую на сетевые интерфейсы, а объявляются протоколом в локальной сети. Если нода, на которую указывает запись в ARP, станет недоступна, то для этого IP-адреса будет объявлен MAC-адрес другой ноды, что обеспечивает отказоустойчивость балансировщика.

Включим фичу, добавив в values.yaml следующее (подразумевается, что замена kube-proxy уже включена):

l2announcements:   enabled: true k8sClientRateLimit:   qps: <qps>   burst: <burts>

Также рекомендую рестартнуть Cilium-агенты и оператор.

k8sClientRateLimit используется для настройки rate-лимитов клиента Kubernetes, так как L2 Announcement может сильно нагружать kube-apiserver. Использовать не обязательно, но пригодится, если Cilium-оператор будет упираться в лимиты. Подробнее об этом можно почитать здесь.

Теперь можно создать политику L2 Announcement:

apiVersion: "cilium.io/v2alpha1" kind: CiliumL2AnnouncementPolicy metadata:   name: l2announcement spec:   serviceSelector:     matchLabels:       app: backend   nodeSelector:     matchExpressions:       - key: node-role.kubernetes.io/control-plane         operator: In         values:         - l2announcement   interfaces:   - ^eth[0-9]+   externalIPs: true   loadBalancerIPs: true

Подробнее о полях:

  • serviceSelector – сервисы, чьи адреса будут объявляться политикой. Если значение не указано, то политика будет распространяться на все сервисы;

  • nodeSelector – ноды, чьи интерфейсы будут использоваться для объявления адресов. Если значение не указано, то политика будет распространяться на все ноды;

  • interfaces – интерфейсы для объявления. Если не указано, будут использованы все интерфейсы;

  • externalIPs и loadBalancerIPs – адреса, которые будут объявлены:

    • в случае externalIPs будут объявлены spec.externalIPs, которые задаются автором сервиса;

    • в случае loadBalancerIPs будет объявлен status.loadbalancer.ingress, который задается пулом CiliumLoadBalancerIPPool.

    Можно использовать оба варианта.

После применения манифеста CiliumL2AnnouncementPolicy в кластере появятся ресурсы Lease с префиксом cilium-l2announce- для каждого сервиса, который будет анонсироваться. К примеру, если у вас включены Ingress-контроллер и Gateway API, это будет выглядеть так:

$ kubectl get lease -A NAMESPACE         NAME                                                  HOLDER                      AGE <...> kube-system       cilium-l2announce-default-cilium-gateway-my-gateway   worker2                     16m kube-system       cilium-l2announce-kube-system-cilium-ingress          worker2                     16m <...>

Lease используется для избрания ноды-лидера, чей MAC-адрес попадет в ARP-таблицу. Как видно, в данном случае оба сервиса будет анонсировать worker2. Для теста отправим запрос по адресу сервиса, посмотрим на записи в ARP и сравним MAC-адрес:

$ arp -a <...> ? (192.168.31.3) at 74:56:3c:67:ec:ca on en0 ifscope [ethernet] <...>  $ ifconfig <...> eno1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500         inet 192.168.31.2  netmask 255.255.255.0  broadcast 0.0.0.0         inet6 fe80::7656:3cff:fe67:ecca  prefixlen 64  scopeid 0x20<link>         ether 74:56:3c:67:ec:ca  txqueuelen 1000  (Ethernet)         RX packets 13313  bytes 3241632 (3.0 MiB)         RX errors 0  dropped 0  overruns 0  frame 0         TX packets 22961  bytes 24963524 (23.8 MiB)         TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

В моем случае адрес сервиса был 192.168.31.3, и он успешно появился в таблице. MAC-адрес интерфейса в записи совпадает с тем, что показал ifconfig, а значит, все успешно заработало.

Service Mesh

Теперь предлагаю рассмотреть Cilium и eBPF в контексте Service Mesh. Я думаю, ни для кого не будет новостью, что самым спорным техническим решением в современных системах Service Mesh (к примеру, Istio и Linkerd) является использование sidecar-прокси. Разумеется, они обладают существенными преимуществами, а именно:

  • позволяют ассоциировать идентичности с подами;

  • позволяют шифровать трафика между подами;

  • дают возможность масштабировать потребление ресурсов отдельно для каждого пода;

  • предоставляют фичу «single-tenancy», благодаря которой падение прокси приводит к недоступности только того пода, в котором он находится.

При этом не менее существенными являются их недостатки:

  • увеличенное потребление ресурсов кластера;

  • увеличенное время старта подов;

  • «race condition» при старте подов;

  • проблемы с ресурсами Job и CronJob;

  • необходимость перезапуска приложения для обновления прокси.

В итоге sidecar-контейнеры воспринимаются скорее как «необходимое зло», с которым приходится мириться, чтобы использовать фичи Service Mesh. Может ли Cilium исправить эту ситуацию и избавить нас от необходимости иметь дело с sidecar-прокси?

Ответ – и да, и нет. Дело в том, что ввиду некоторых технических ограничений, на которых построен eBPF, в него пока сложно перенести большую часть функционала уровня L7 (подробнее об этом рассказал Ювал Кохави из Solo.io в своем выступлении). Поэтому Cilium приходится использовать Envoy для L7, в то время как функционал уровней L3 и L4 перенесен в ядро и работает в eBPF.

Как я уже упомянул выше, Envoy в Cilium развертывается на каждую ноду как DaemonSet, либо как вшитый в Cilium-агент. Похожим образом работает новый Ambient-режим в Istio, где прокси разворачивается либо на каждую ноду, либо на namespace. Несмотря на то, что такой подход решает некоторые проблемы sidecar’ов, он также не идеален и имеет свои издержки:

  • непредсказуемое потребление ресурсов у прокси;

  • проблемы «noisy neighbour»: прокси должен обеспечивать QoS для всех тенантов;

  • падение и обновление прокси влияет на все его тенанты.

Таким образом, Cilium избавился от sidecar’ов, но не избавился от прокси, поэтому в ближайшее время нам не избежать их присутствия в кластерах. Вопрос в том, какой скоуп для них использовать: на каждый под, каждый неймспейс или каждую ноду? Все варианты по-своему хороши и плохи, поэтому выбор придется делать, исходя из конкретных целей и задач.

Control plane

Control plane в Cilium состоит из следующих компонентов:

  1. Ingress;

  2. Gateway API;

  3. CiliumNetworkPolicy;

  4. CiliumEnvoyConfig;

  5. CiliumClusterideEnvoyConfig.

О первых трех пунктах я уже рассказал выше. По задумке разработчиков, Cilium не предоставляет наполненный фичами Control plane, как, например, в Istio, но предоставляет платформу для сторонних Control plane’ов с помощью ресурсов CiliumEnvoyConfig и CiliumClusterideEnvoyConfig. Они дают прямой доступ к конфигу Envoy и позволяют создавать своих listener’ов.

Cilium Ingress и Gateway API также используют эти ресурсы. Например, если создать Ingress для Hubble UI, его CiliumEnvoyConfig будет выглядеть так:

CiliumEnvoyConfig
apiVersion: cilium.io/v2 kind: CiliumEnvoyConfig metadata:   creationTimestamp: "2024-04-13T17:12:39Z"   generation: 5   labels:     cilium.io/use-original-source-address: "false"   name: cilium-ingress   namespace: kube-system   resourceVersion: "1459379"   uid: 71deeebf-6436-49d0-bd1b-5fd3f8e81b51 spec:   backendServices:   - name: hubble-ui     namespace: kube-system     number:     - http   resources:   - '@type': type.googleapis.com/envoy.config.listener.v3.Listener     filterChains:     - filterChainMatch:         transportProtocol: raw_buffer       filters:       - name: envoy.filters.network.http_connection_manager         typedConfig:           '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager           commonHttpProtocolOptions:             maxStreamDuration: 0s           httpFilters:           - name: envoy.filters.http.grpc_web             typedConfig:               '@type': type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb           - name: envoy.filters.http.grpc_stats             typedConfig:               '@type': type.googleapis.com/envoy.extensions.filters.http.grpc_stats.v3.FilterConfig               emitFilterState: true               enableUpstreamStats: true           - name: envoy.filters.http.router             typedConfig:               '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router           rds:             routeConfigName: listener-insecure           statPrefix: listener-insecure           upgradeConfigs:           - upgradeType: websocket           useRemoteAddress: true     listenerFilters:     - name: envoy.filters.listener.tls_inspector       typedConfig:         '@type': type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector     name: listener     socketOptions:     - description: Enable TCP keep-alive (default to enabled)       intValue: "1"       level: "1"       name: "9"     - description: TCP keep-alive idle time (in seconds) (defaults to 10s)       intValue: "10"       level: "6"       name: "4"     - description: TCP keep-alive probe intervals (in seconds) (defaults to 5s)       intValue: "5"       level: "6"       name: "5"     - description: TCP keep-alive probe max failures.       intValue: "10"       level: "6"       name: "6"   - '@type': type.googleapis.com/envoy.config.route.v3.RouteConfiguration     name: listener-insecure     virtualHosts:     - domains:       - hubble.test.local       - hubble.test.local:*       name: hubble.test.local       routes:       - match:           prefix: /         route:           cluster: kube-system:hubble-ui:http   - '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster     connectTimeout: 5s     edsClusterConfig:       serviceName: kube-system/hubble-ui:http     name: kube-system:hubble-ui:http     outlierDetection:       splitExternalLocalOriginErrors: true     type: EDS     typedExtensionProtocolOptions:       envoy.extensions.upstreams.http.v3.HttpProtocolOptions:         '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions         commonHttpProtocolOptions:           idleTimeout: 60s         useDownstreamProtocolConfig:           http2ProtocolOptions: {}   services:   - listener: ""     name: cilium-ingress     namespace: kube-system

Говоря о сторонних Control plane’ах, у Cilium есть интеграция с Istio как в режиме sidecar, так и в режиме ambient. С точки зрения администратора, установка и эксплуатация Istio с Cilium практически не отличается от установки и эксплуатации c любым другим CNI. Но есть пара нюансов:

  • если включена замена kube-proxy, то в values.yaml Cilium нужно включить socketLB.hostNamespaceOnly: true;

  • Для управления трафиком уровня L7 лучше не использовать Cilium и Istio одновременно, чтобы избежать «split-brain» проблем.

Послесловие

Разумеется, в рамках одной статьи невозможно разобрать все фичи Cilium. Я постарался сосредоточиться на тех, что показались мне наиболее полезными и универсальными. Если вы хотите изучить возможности этого плагина глубже, то можете почитать еще и о том, как объединить несколько кластеров в общую сеть (Cluster mesh) или настроить шифрование трафика между нодами с помощью IPsec и Wireguard.

Тем не менее, я надеюсь, что эта обзорная статья помогла вам ознакомиться с плагином. Несмотря на некоторые мелкие неудобства, стоит сказать, что проект стремительно развивается и вместе с eBPF уже сейчас значительно упрощает работу с сетью в Kubernetes. Вкупе с другими фичами, эта технология заметно выделяет Cilium на фоне других популярных CNI-проектов вроде Calico и Flannel.

Для тех, кто также интересуется другими аспектами работы с Kubernetes, предлагаю ознакомиться с нашими материалами на эту тему:

Если же материал показался вам слишком сложным, даже не думайте опускать руки. Вы можете ознакомиться с нашими статьями о Kubernetes для начинающих бэкэнд-разработчиков, после чего вернуться к изучению CNI-плагинов:


ссылка на оригинал статьи https://habr.com/ru/articles/825136/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *