Calico для мережі в Kubernetes: знайомство та трохи з досвіду

Calico для мережі в Kubernetes: знайомство та трохи з досвіду

Мета статті – познайомити читача з основами мережевої взаємодії та управлінням мережевими політиками в Kubernetes, а також зі стороннім плагіном Calico, що розширює стандартні можливості. Принагідно буде продемонстровано зручність конфігурації та деякі фічі на реальних прикладах з досвіду нашої експлуатації.

Швидке введення в мережевий пристрій Kubernetes

Кластер Kubernetes неможливо уявити без мережі. Ми вже публікували матеріали з їх основ: «Ілюстрований посібник з влаштування мережі в Kubernetes» та «Введення в мережеві політики Kubernetes для фахівців з безпеки».

У контексті цієї статті важливо відзначити, що за мережеву зв'язність між контейнерами та вузлами відповідає не сам K8s: для цього використовуються всілякі плагіни CNI (Container Networking Interface). Докладніше про цю концепцію ми теж розповідали.

Наприклад, найпоширеніший із таких плагінів — Фланель - Забезпечує повну мережеву зв'язність між усіма вузлами кластера за допомогою підняття мостів на кожному вузлі, закріплюючи за ним підсіти. Однак повна та нерегульована доступність не завжди корисна. Щоб забезпечити якусь мінімальну ізоляцію в кластері, необхідно втрутитися у конфігурування 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).

Calico для мережі в Kubernetes: знайомство та трохи з досвіду

Власне, на ці дві категорії за напрямом руху і поділяється політика.

Наступний обов'язковий атрибут – селектор; той, до кого застосовується правило. Це може бути pod (або група pod'ів) або оточення (тобто простір імен). Важлива деталь: обидва види цих об'єктів повинні містити мітку (етикетка у термінології Kubernetes) - саме ними оперують політики.

Крім кінцевого числа селекторів, об'єднаних якоюсь міткою, існує можливість написання правил на кшталт «Дозволити/заборонити все/всім» у різних варіаціях. Для цього використовуються конструкції виду:

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

- У цьому прикладі всім pod'ам оточення закривається вхідний трафік. Протилежної поведінки можна досягти такою конструкцією:

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

Аналогічно для вихідного:

  podSelector: {}
  policyTypes:
  - Egress

- Для його відключення. І ось що для включення:

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

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

Calico для мережі в Kubernetes: знайомство та трохи з досвіду

Знайомимося з Calico: теорія

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

Які можливості дає використання «коробкового» рішення K8s та набору API із Calico?

Ось що вбудоване в NetworkPolicy:

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

А ось як Calico розширює ці функції:

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

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

Calico для мережі в Kubernetes: знайомство та трохи з досвіду

Багато великих managed-рішення з K8s, такі як Amazon EKS, Azure AKS, Google GKE та інші стали рекомендувати його до використання.

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

Calico для мережі в Kubernetes: знайомство та трохи з досвіду

Проект швидко розвивається, підтримується робота в популярних рішеннях managed K8s, OpenShift, OpenStack, є можливість використовувати Calico при розгортанні кластера за допомогою удар ногою, зустрічаються згадки побудови 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 від стандартних і як підхід до складання правил полегшує їх читання та гнучкість конфігурування:

Calico для мережі в Kubernetes: знайомство та трохи з досвіду

У кластері розгорнуто 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. У розвантаженні можуть бути чутливі дані, які за замовчуванням знову ж таки доступні для загального огляду. Закриємо від сторонніх очей ці дані:

Calico для мережі в Kubernetes: знайомство та трохи з досвіду

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-тунель, і цей доступ жорстко контролюємо і обмежений конкретним списком дозволених до використання сервісів:

Calico для мережі в Kubernetes: знайомство та трохи з досвіду

Клієнти підключаються до 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.

PS

Читайте також у нашому блозі:

Джерело: habr.com

Додати коментар або відгук