29 January

Calico для сети в Kubernetes: знакомство и немного из опыта

Флант corporate blogSystem administrationNetwork technologiesKubernetes


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

Быстрое введение в сетевое устройство Kubernetes


Кластер Kubernetes невозможно представить без сети. Мы уже публиковали материалы по их основам: «Иллюстрированное руководство по устройству сети в Kubernetes» и «Введение в сетевые политики Kubernetes для специалистов по безопасности».

В контексте этой статьи важно отметить, что за сетевую связность между контейнерами и узлами отвечает не сам K8s: для этого используются всевозможные плагины CNI (Container Networking Interface). Подробнее об этой концепции мы тоже рассказывали.

К примеру, наиболее распространенный из таких плагинов — Flannel — обеспечивает полную сетевую связность между всеми узлами кластера с помощью поднятия мостов на каждом узле, закрепляя за ним подсеть. Однако полная и нерегулируемая доступность не всегда полезна. Чтобы обеспечить какую-то минимальную изоляцию в кластере, необходимо вмешаться в конфигурирование firewall’а. В общем случае оно отдано в управление того самого CNI, из-за чего любые сторонние вмешательства в iptables могут быть интерпретированы некорректно или игнорироваться вовсе.

А «из коробки» для организации управления сетевыми политиками в кластере Kubernetes предоставляется NetworkPolicy API. Этот ресурс, распространяющийся на выбранные пространства имён, может содержать правила для разграничения доступа от одних приложений к другим. Он также позволяет настраивать доступность между конкретными pod’ами, окружениями (пространствами имён) или блоками IP-адресов:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: test-network-policy
  namespace: default
spec:
  podSelector:
    matchLabels:
      role: db
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - ipBlock:
        cidr: 172.17.0.0/16
        except:
        - 172.17.1.0/24
    - namespaceSelector:
        matchLabels:
          project: myproject
    - podSelector:
        matchLabels:
          role: frontend
    ports:
    - protocol: TCP
      port: 6379
  egress:
  - to:
    - ipBlock:
        cidr: 10.0.0.0/24
    ports:
    - protocol: TCP
      port: 5978

Этот не самый примитивный пример из официальной документации может раз и навсегда отбить желание разбираться в логике работы сетевых политик. Однако мы всё же попробуем понять основные принципы и методы обработки потоков трафика с помощью сетевых политик…

Логично, что есть 2 типа трафика: входящий в pod (Ingress) и исходящий из него (Egress).



Собственно, на эти 2 категории по направлению движения и разделяется политика.

Следующий обязательный атрибут — селектор; тот, к кому применяется правило. Это может быть pod (или группа pod’ов) или окружение (т.е. пространство имен). Важная деталь: оба вида этих объектов обязаны содержать метку (label в терминологии Kubernetes) — именно ими оперируют политики.

Помимо конечного числа селекторов, объединенных какой-то меткой, существует возможность написания правил вроде «Разрешить/запретить всё/всем» в разных вариациях. Для этого используются конструкции вида:

  podSelector: {}
  ingress: []
  policyTypes:
  - Ingress

— в этом примере всем pod’ам окружения закрывается входящий трафик. Противоположного поведения можно добиться такой конструкцией:

  podSelector: {}
  ingress:
  - {}
  policyTypes:
  - Ingress

Аналогично для исходящего:

  podSelector: {}
  policyTypes:
  - Egress

— для его отключения. И вот что для включения:

  podSelector: {}
  egress:
  - {}
  policyTypes:
  - Egress

Возвращаясь к выбору CNI-плагина для кластера, стоит отметить, что не каждый сетевой плагин поддерживает работу с NetworkPolicy. Например, уже упомянутый Flannel не умеет конфигурировать сетевые политики, о чём прямо сказано в официальном репозитории. Там же упомянута альтернатива — Open Source-проект Calico, который заметно расширяет стандартный набор API Kubernetes в плане сетевых политик.



Знакомимся с Calico: теория


Плагин Calico может использоваться в интеграции с Flannel (подпроект Canal) или самостоятельно, покрывая как функции по обеспечению сетевой связности, так и возможности управления доступностью.

Какие возможности даёт использование «коробочного» решения K8s и набора API из Calico?

Вот что встроено в NetworkPolicy:

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

А вот как Calico расширяет эти функции:

  • политики могут применяться к любому объекту: pod, контейнер, виртуальная машина или интерфейс;
  • правила могут содержать конкретное действие (запрет, разрешение, логирование);
  • в качестве цели или источника правил может быть порт, диапазон портов, протоколы, HTTP- или ICMP-атрибуты, IP или подсеть (4 или 6 поколения), любые селекторы (узлов, хостов, окружений);
  • дополнительно можно регулировать прохождение трафика с помощью настроек DNAT и политик проброса трафика.

Первые коммиты на GitHub в репозитории Сalico датируются июлем 2016 года, а уже через год проект занял лидирующие позиции в организации сетевой связности Kubernetes — об этом гласят, например, результаты опроса, проведенного The New Stack:



Многие крупные managed-решения с K8s, такие как Amazon EKS, Azure AKS, Google GKE и другие, стали рекомендовать его к использованию.

Что касается производительности, тут всё замечательно. При тестировании своего продукта команда разработки Calico продемонстрировала астрономические показатели, запустив более 50000 контейнеров на 500 физических узлах со скоростью создания 20 контейнеров в секунду. Проблем при масштабировании не выявлено. Такие результаты были озвучены уже при анонсе первой версии. Независимые исследования, направленные на пропускную способность и объемы потребления ресурсов, также подтверждают производительность Calico, практически не уступающую Flannel. Например:



Проект очень быстро развивается, поддерживается работа в популярных решениях managed K8s, OpenShift, OpenStack, имеется возможность использовать Calico при разворачивании кластера с помощью kops, встречаются упоминания построения Service Mesh-сетей (вот пример использования совместно с Istio).

Практика с Calico


В общем случае использования ванильного Kubernetes установка CNI сводится к применению файла calico.yaml, скачанного с официального сайта, с помощью kubectl apply -f.

Как правило, актуальная версия плагина совместима с 2-3 последними версиями Kubernetes: работу в более старых версиях не тестируют и не гарантируют. По заявлениям разработчиков, Calico работает на ядре Linux выше 3.10 под управлением CentOS 7, Ubuntu 16 или Debian 8, поверх iptables или IPVS.

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


Для общего понимания рассмотрим простой случай, чтобы понять, чем отличаются сетевые политики в нотации Calico от стандартных и как подход к составлению правил упрощает их читаемость и гибкость конфигурирования:



В кластере развёрнуты 2 веб-приложения: на Node.js и PHP, — одно из которых использует Redis. Чтобы закрыть доступ к Redis из PHP, оставив при этом связность с Node.js, достаточно применить следующую политику:

kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
  name: allow-redis-nodejs
spec:
  podSelector:
    matchLabels:
      service: redis
  ingress:
  - from:
    - podSelector:
        matchLabels:
          service: nodejs
    ports:
    - protocol: TCP
      port: 6379

По сути мы разрешили входящий трафик на порт Redis из Node.js. И явно не запрещали ничего другого. Как только появляется NetworkPolicy, то все селекторы, упомянутые в нём, начинают изолироваться, если не указано иное. При этом правила изоляции не распространяются на другие объекты, не покрываемые селектором.

В примере используется apiVersion Kubernetes’а «из коробки», но ничто не мешает использовать одноименный ресурс из поставки Calico. Синтаксис там более развёрнутый, поэтому потребуется переписать правило для вышеописанного случая в следующем виде:

apiVersion: crd.projectcalico.org/v1
kind: NetworkPolicy
metadata:
  name: allow-redis-nodejs
spec:
  selector: service == 'redis'
  ingress:
  - action: Allow
    protocol: TCP
    source:
      selector: service == 'nodejs'
    destination:
      ports:
      - 6379

Упомянутые выше конструкции для разрешения или запрета всего трафика посредством обычного NetworkPolicy API содержат сложные для восприятия и запоминания конструкции со скобками. В случае с Calico, чтобы изменить логику работы правила firewall’а на противоположную, достаточно сменить action: Allow на action: Deny.

Изоляция по окружениям


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



Prometheus, как правило, вынесен в отдельное служебное окружение — в примере это будет namespace следующего вида:

apiVersion: v1
kind: Namespace
metadata:
  labels:
    module: prometheus
  name: kube-prometheus

Поле metadata.labels тут оказалось не случайно. Как выше уже упоминалось, namespaceSelector (как и podSelector) оперирует лейблами. Поэтому, чтобы разрешить забирать метрики со всех pod’ов на определенном порту, придется добавить какую-нибудь метку (или взять из существующих), а затем применить конфигурацию вроде:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-metrics-prom
spec:
  podSelector: {}
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          module: prometheus
    ports:
    - protocol: TCP
      port: 9100

А в случае использования политик Calico синтаксис будет таким:

apiVersion: crd.projectcalico.org/v1
kind: NetworkPolicy
metadata:
  name: allow-metrics-prom
spec:
  ingress:
  - action: Allow
    protocol: TCP
    source:
      namespaceSelector: module == 'prometheus'
    destination:
      ports:
      - 9100

В целом, добавляя подобного рода политики под конкретные нужды, можно оградить от злонамеренного или случайного вмешательства в работу приложений в кластере.

Лучшей практикой, по мнению создателей Calico, является подход «Запрети всё и явно открывай необходимое», зафиксированный в официальной документации (аналогичного подхода придерживаются и другие — в частности, в уже упомянутой статье).

Применение дополнительных объектов Calico


Напомню, что посредством расширенного набора API Calico можно регулировать доступность узлов, не ограничиваясь pod’ами. В следующем примере с помощью GlobalNetworkPolicy закрывается возможность прохождения ICMP-запросов в кластере (например, пинги из pod’а на узел, между pod’ми или с узла на IP pod’а):

apiVersion: crd.projectcalico.org/v1
kind: GlobalNetworkPolicy
metadata:
  name: block-icmp
spec:
  order: 200
  selector: all()
  types:
  - Ingress
  - Egress
  ingress:
  - action: Deny
    protocol: ICMP
  egress:
  - action: Deny
    protocol: ICMP

В приведенном выше кейсе остается возможность узлам кластера «достучаться» между собой по ICMP. И этот вопрос решается средствами GlobalNetworkPolicy, примененной к сущности HostEndpoint:

apiVersion: crd.projectcalico.org/v1
kind: GlobalNetworkPolicy
metadata:
  name: deny-icmp-kube-02
spec:
  selector: "role == 'k8s-node'"
  order: 0
  ingress:
  - action: Allow
    protocol: ICMP
  egress:
  - action: Allow
    protocol: ICMP
---
apiVersion: crd.projectcalico.org/v1
kind: HostEndpoint
metadata:
  name: kube-02-eth0
  labels:
    role: k8s-node
spec:
  interfaceName: eth0
  node: kube-02
  expectedIPs: ["192.168.2.2"]

Случай с VPN


Наконец, приведу вполне реальный пример использования функций Calico для случая с околокластерным взаимодействием, когда стандартного набора политик не хватает. Для доступа к веб-приложению клиентами используется VPN-туннель, и этот доступ жестко контролируем и ограничен конкретным списком разрешенных к использованию сервисов:



Клиенты подключаются к VPN через стандартный UDP-порт 1194 и при подключении получают маршруты к кластерным подсетям pod’ов и сервисов. Подсети push’атся целиком, чтобы не терять сервисы при перезапусках и смене адресов.

Порт в конфигурации — стандартный, что накладывает некоторые нюансы на процесс конфигурирования приложения и его перенос в Kubernetes-кластер. К примеру, в том же AWS LoadBalancer для UDP появился буквально в конце прошлого года в ограниченном списке регионов, а NodePort нельзя использовать из-за его проброса на всех узлах кластера и невозможно масштабировать количество инстансов сервера в целях отказоустойчивости. Плюс, придется менять диапазон портов, выбираемый по умолчанию…

В результате перебора возможных решений было выбрано следующее:

  1. Pod’ы с VPN планируются на узел в режиме hostNetwork, то есть на фактический IP.
  2. Сервис вывешивается наружу через ClusterIP. На узле физически поднимается порт, который доступен извне с небольшими оговорками (условное наличие реального IP-адреса).
  3. Определение узла, на котором поднялся pod, лежит за пределами нашего повествования. Скажу лишь, что можно жестко «прибить» сервис к узлу или же написать небольшой sidecar-сервис, который будет следить за текущим IP-адресом VPN-сервиса и править DNS-записи, прописанные у клиентов — у кого на что хватит фантазии.

С точки зрения маршрутизации мы можем однозначно идентифицировать клиента за VPN по его IP-адресу, выдаваемому сервером VPN. Ниже — примитивный пример ограничения доступа такому клиенту к сервисам, иллюстрация на вышеупомянутом Redis:

apiVersion: crd.projectcalico.org/v1
kind: HostEndpoint
metadata:
  name: vpnclient-eth0
  labels:
    role: vpnclient
    environment: production
spec:
  interfaceName: "*"
  node: kube-02
  expectedIPs: ["172.176.176.2"]
---
apiVersion: crd.projectcalico.org/v1
kind: GlobalNetworkPolicy
metadata:
  name: vpn-rules
spec:
  selector: "role == 'vpnclient'"
  order: 0
  applyOnForward: true
  preDNAT: true
  ingress:
  - action: Deny
    protocol: TCP
    destination:
      ports: [6379]
  - action: Allow
    protocol: UDP
    destination:
      ports: [53, 67]

Здесь жестко запрещается подключение на порт 6379, но при этом сохранена работа службы DNS, функционирование которой довольно часто страдает при составлении правил. Потому что, как ранее упоминалось, при появлении селектора к нему применяется запретительная политика по умолчанию, если не указано иное.

Итоги


Таким образом, с помощью расширенного API Calico можно гибко конфигурировать и динамически менять маршрутизацию в кластере и вокруг него. В общем случае его использование может выглядеть как стрельба из пушки по воробьям, а внедрение L3-сети с BGP- и IP-IP-туннелями выглядит монструозно в простой инсталляции Kubernetes в плоской сети… Однако в остальном инструмент выглядит вполне жизнеспособным и полезным.

Изоляция кластера для обеспечения требований безопасности не всегда может быть реализуема, и именно в таких случаях на помощь приходит Calico (или подобное решение). Приведенные в статье примеры (с небольшой доработкой) используются в нескольких инсталляциях наших клиентов в AWS.

P.S.


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

Tags:CalicoKubernetesCNI
Hubs: Флант corporate blog System administration Network technologies Kubernetes
+38
6.7k 74
Leave a comment
Ads