Привет! Меня зовут Даниил, я 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), который замеряет следующую последовательность:
-
открывается TCP-соединение;
-
отправляется единичный запрос;
-
приходит ответ;
-
соединение закрывается.
Проведем похожий тест и сравним результаты. Для простоты исключим 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 состоит из двух компонентов:
-
Hubble-сервер, вшитый в Cilium-агент. Он отвечает за мониторинг трафика ноды, на которой он развернут.
-
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-кластер, который не находится под управлением облачного провайдера, то для доступа своих сервисов извне вы, скорее всего, будете использовать один из следующих вариантов:
-
NodePort
илиhostNetwork
; -
NodePort
илиhostNetwork
с внешним балансировщиком; -
балансировщик 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 состоит из следующих компонентов:
-
Ingress;
-
Gateway API;
-
CiliumNetworkPolicy;
-
CiliumEnvoyConfig;
-
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-плагинов:
Автор: Даниил Локтев