company_banner

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



    Цель статьи — познакомить читателя с основами сетевого взаимодействия и управлением сетевыми политиками в 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.


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

    Флант
    Специалисты по DevOps и Kubernetes

    Комментарии 0

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

    Самое читаемое