О внутрикластерной маршрутизации через Istio
Привет, Хабр! Я Степан, DevOps-инженер, занимаюсь созданием CI/CD процессов с учётом проверки кода на безопасность, поддержкой и разверткой новых кластеров Kubernetes, соблюдением требований безопасности и созданием системы мониторинга и логирования — все это в рамках одной команды. Хочу рассказать об одной незадокументированной особенности Istio, с которой мне довелось столкнуться при внедрении Service Mesh.
Об Istio на Хабре написано уже немало (например, тут и тут, может пригодиться новичкам в этой теме), но вот разбора конкретных примеров не хватает. И когда у нас в ОТП Банке возникла проблема с «Истио», решать её нашей команде пришлось самостоятельно.
Быть может, наш опыт пригодится тому, кто позже наступит на те же грабли. Даже лучше будет, если кто-то прочитает и уже не наступит. Конкретно наши грабли пригодятся при использовании Istio для маршрутизации в рамках одного кластера. Ещё наш кейс можно проецировать на другие задачи с маршрутизацией. И использовать как пример того, как в нестандартных ситуациях искать качественное решение.
Как и статьи по ссылкам выше, этот текст в основном для начинающих. Эксперты, вероятно, и без того в курсе (возможно, даже подскажут более эффективное решение). С экспертизой и более глубокой работой с istio + может быть с envoy данное решение будет найти не так сложно. Под катом — подробности проблемы, схема нашего конфига с решением и код.
С чего всё начиналось
История началась весной. До того мы использовали в работе Bare metal с NGINX, то есть физический кластер в ЦОД с развёрнутым Kubernetes на нём. Увеличилась потребность в ресурсах и были выделены под это новые сервера. Всё осталось на Baremetal. В процессе наша конфигурация изменилась и стала включать два кластера Kubernetes на разных дата-центрах. Пришлось задуматься о более эффективной и удобной маршрутизации.
Выбор инструмента оказался несложным: в финтехе есть списки одобренного ПО, отвечающего требованиям безопасности нашей компании. Istio в своей категории был один, и, кстати, по части безопасности с ним действительно оказалось просто: предложенные решения «из коробки» нас вполне устраивали. Решение Istio как некая замена Ingress контроллеру (всё-таки принцип конфигурации у них отличается, как минимум наличием доп. сущностей Kubernetes (VirtualService, Gateway)), так же он обеспечивает защиту и мониторинг трафика внутри кластера, что не может не радовать. Кроме того, Istio уже был на слуху, по нему была хорошая документация, и всё выглядело так, будто никаких проблем ожидать не стоит.
Мигрировали на новые кластеры мы постепенно: приложение за приложением. До тех пор, пока кластеры работали с ними как с внешними сервисами, всё было хорошо. Так прошли первые 3 месяца. А потом мигрировало приложение с запросами в пределах своего кластера… и всё сломалось. Оказалось, Istio просто не маршрутизирует запросы внутри кластера. Буквально: Istio не отправлял трафик на новые версии приложения.
Изначальная конфигурация:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
labels:
app: my-app
name: my-app-traffic-balancing
namespace: my-app-ns
spec:
gateways:
- default/https-gateway
hosts:
- "hostname"
http:
- match:
- uri:
prefix: /my-app/
rewrite:
uri: /
route:
- destination:
host: my-app-prod.my-app-ns.svc.cluster.local #Как видно, мы ссылаемся на конкретный сервис.
weight: 99
- destination:
host: my-app-prod-new.my-app-ns.svc.cluster.local #И перенаправляем трафик уже на сервис нового приложения, в данном случае перенаправляется 1% трафика.
weight: 1
---
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: https-gateway
spec:
selector:
istio: ingressgateway
servers:
- hosts:
- '*'
port:
name: https
number: 443
protocol: HTTPS
tls:
credentialName: my-secret-cert # secret содержащий сертификат
mode: SIMPLE
Это, к счастью, катастрофой не стало: мы практикуем подобие «канареечного» подхода к выкатке, хотя можно назвать его и A/B-подходом. То есть, мы раскатываем обновлённые приложения по соседству с основными, отдельно, и заранее проверяем, что всё работает. Так что, когда не заработало, не пришлось метаться и искать корень проблемы здесь и сейчас. И это очень хорошо, потому что поиск занял у меня немало времени.
В документации всё есть (нет)
Первое, что я сделал, — поднял документацию и попытался настроить маршрутизацию с помощью ServiceEntry и DestinationRule, чтобы расставить метки и указать точные адреса сервисов и внешних серверов. По всем признакам это должно было сработать, но не сработало. Крайне неприятно чувствовать, что вроде бы делаешь всё, как описано в документации, а результата всё равно нет.
В Istio много возможностей и сервисов, но для нашего случая особенно важно сказать о VirtualService. Как подсказывает название, это виртуальный сервис, который на основе Service строит свою маршрутизацию. По документации у него должны быть Gateway, с помощью которых открываются порты наружу. Вы можете открыть, допустим, 443-й или 80-й порт на Gateway этого виртуального сервиса и по этому порту обращаться в кластер.
Именно VirtualService я и настраивал, варьируя DestinationRule. Самодиагностика средствами Istio при этом проблем не показывала, кластер подтверждал, что соединение с Istio есть, но маршрутизации не было. Тем не менее несколько напрягало менять конфигурации почти вслепую, не понимая, что может не устраивать Istio. Мы с командой в том числе направляли трафик на разные сервисы, пытались его размаршрутизировать. Так я без особого успеха бился над Istio около двух недель.
Думайте сами, решайте сами
Помимо документации, я искал ответы где только мог. Например, в книге «Istio: приступаем к работе», Калькот Л., Бутчер З. В ней описаны принципы работы «Истио» и приведены примеры конфигурации. Единственное, что книжка уже отстала от текущей версии «Истио» и нужно ориентироваться больше на документацию. Наша проблема нигде не упоминалась. Однако размышления над схемами настроек даром не прошли: в какой-то момент меня озарило, как всё работает и где происходит сбой.
Если у VirtualService есть указанный Gateway, сервис будет ждать, что к нему придёт запрос извне. Можно отправить ему внутренний запрос, он ответит, что всё в порядке… и продолжит ждать внешнего сигнала. Это логично, если у вас трафик приходит из другого кластера или с другого сервера или вообще пользователь заходит через браузер. Но при маршрутизации внутри кластера надо принудительно отвязать VirtualService от Gateway: не указывать их.
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
labels:
app: my-app
name: my-app-balancing-inside
namespace: my-app-ns
spec:
hosts:
- my-app-service.my-app-ns.svc.cluster.local # Важно, в данном VirtualService отсутствует привязка к GateWay.
http:
- match:
- uri:
prefix: /
name: my-app-dr
route:
- destination:
host: my-app-service.my-app-ns.svc.cluster.local # В отличие от предыдущего примера направляем трафик на один сервис.
port:
number: 8443
subset: v1
weight: 99
- destination:
host: my-app-service.my-app-ns.svc.cluster.local # В отличие от предыдущего примера направляем трафик на один сервис.
port:
number: 8443
subset: v2
weight: 1
Как видно, в данной конфигурации указано поле subset об этом будет далее.
Конечно, при этом сервис должен общаться с приложениями — и тут поможет DestinationRule. Так как виртуальный сервис ссылается на Service Kubernetes, нам нужно, чтобы он (Service) сам по себе видел все версии приложения, — для этого как раз подойдут метки (labels). Это выглядит так в Deployment-е приложений мы проставляем разные метки: для прод-версии — version: v1, для новой версии — version: v2. При этом хорошо бы не забыть проставить общую метку для Service, например — app: my-app. Это позволит в дальнейшем при задании поля selector определить, по каким меткам выбирать созданные Pod. Далее с помощью DestinationRule мы определяем так называемый subset:
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: my-app-dr
namespace: my-app-ns
spec:
host: my-app-service.my-app-ns.svc.cluster.local
subsets:
- labels:
app: my-app
version: v1
name: v1 # Данное имя указывается в VirtualService.
- labels:
app: my-app
version: v2
name: v2 # Данное имя указывается в VirtualService.
---
Важно, чтобы сервис my-app-service сам по себе маршрутизировал трафик на обе версии приложения, достигается это настройкой поля selector в Service.
...
selector:
app: my-app
...
При этом в Deployment нужно указать дополнительные лейблы, чтобы в последующем DestinationRule смог корректно определить subsets.
Приложение v1
...
labels:
app: my-app
version: v1
...
Приложение v2
...
labels:
app: my-app
version: v2
...
Этого достаточно, чтобы маршрутизация работала корректно, и каждая версия получала только свои данные.
А как же безопасность?
Естественно, важной составляющей настройки Istio является обеспечение безопасности. Один из способов обеспечения безопасности в Istio — это внедрение механизмов взаимной аутентификации и шифрования трафика, таких как mTLS (Mutual TLS). mTLS обеспечивает аутентификацию как клиента, так и сервера, устанавливая двустороннее TLS-шифрование между ними.
Все эти нюансы можно настроить с помощью PeerAuthentication — одного из важных ресурсов в Istio. Этот инструмент позволяет точно настроить, как приложения взаимодействуют между собой. Мы можем сделать так, чтобы нагрузка строго требовала использования mTLS или была более гибкой, допуская незашифрованный трафик.
Интересно, что настройки можно применять на разных уровнях:
· Глобально для всей сети сервисов.
· Для конкретного пространства имен.
· И для отдельных приложений по их селекторам.
Для усиления безопасности нашей сети мы можем запретить передачу незашифрованного трафика. Создадим политику, которая жестко устанавливает требование использования mTLS для всей сети. Это действие — шаг в сторону большей безопасности. Однако, стоит помнить, что радикальные изменения могут вызвать сложности в текущих проектах, где требуется внедрение изменений согласованно между различными командами. Поэтому, более гибкий подход — постепенное ужесточение ограничений с установкой временных рамок для миграции сервисов. Режим PERMISSIVE mTLS позволяет именно это: он дает возможность приложениям принимать как зашифрованный, так и незашифрованный трафик, обеспечивая переходный период.
Вот примеры.
Запрещаем передачу незашифрованного трафика для всей сети:
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: strict-all
namespace: istio-system
spec:
mtls:
mode: STRICT
Разрешаем передачу незашифрованного трафика для определенного неймспейса:
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: permissive-all
namespace: my-app-ns
spec:
mtls:
mode: PERMISSIVE
Счастливый конец
Мы внедрили решение, и проблема с маршрутизацией пропала: уже несколько месяцев наблюдаю, что всё работает как надо.
От себя добавлю, что вышеописанное поведение Istio даже кажется логичным, если знать о нём заранее. Так что, надеюсь, наша статья будут полезна девопсам, которые собираются с помощью Istio маршрутизировать трафик в пределах одного и того же кластера Kubernetes.