Calico pour le réseau dans Kubernetes : introduction et quelques expériences

Calico pour le réseau dans Kubernetes : introduction et quelques expériences

L'objectif de cet article est d'introduire le lecteur aux bases de l'interaction réseau et à la gestion des politiques réseau dans Kubernetes, ainsi qu'à Calico, un plugin tiers qui étend les fonctionnalités standard. En chemin, nous démontrerons la facilité de configuration et certaines fonctionnalités à l'aide d'exemples concrets de notre expérience d'exploitation.

Introduction rapide à l'architecture réseau de Kubernetes

Il est impossible d'imaginer un cluster Kubernetes sans son réseau. Nous avons déjà publié des articles sur les bases de cela : «Guide illustré sur l'architecture réseau dans Kubernetes» et «Introduction aux politiques réseau Kubernetes pour les professionnels de la sécurité».

Dans le contexte de cet article, il est important de souligner que la connectivité réseau entre les conteneurs et les nœuds n'est pas gérée par Kubernetes lui-même : divers plugins CNI (Container Networking Interface) s'en chargent. Nous en avons également parlé.

Par exemple, l'un des plugins les plus courants est Flannel qui assure une connectivité réseau complète entre tous les nœuds du cluster en créant des ponts sur chaque nœud, assignant un sous-réseau à chacun. Cependant, une disponibilité complète et non régulée n'est pas toujours bénéfique. Pour garantir un certain niveau d'isolation dans le cluster, il est nécessaire d'intervenir dans la configuration du pare-feu. En général, cela est laissé à la gestion de ce fameux CNI, rendant tout ajustement tiers des iptables potentiellement mal interprété ou complètement ignoré.

Et « par défaut », la gestion des politiques réseau dans un cluster Kubernetes est fournie par l'API NetworkPolicy. Cette ressource, qui s'applique aux espaces de noms sélectionnés, peut contenir des règles pour délimiter l'accès entre différentes applications. Elle permet également de définir l'accessibilité entre des pods spécifiques, des environnements (espaces de noms) ou des blocs d'adresses 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

Cet exemple, qui n'est pas le plus trivial, la documentation officielle Cela peut définitivement dissuader de vouloir comprendre la logique des politiques réseau. Cependant, nous allons tout de même essayer de comprendre les principes de base et les méthodes de traitement du trafic à l'aide des politiques réseau...

Il est logique qu'il existe deux types de trafic : celui entrant dans le pod (Ingress) et celui sortant de celui-ci (Egress).

Calico pour le réseau dans Kubernetes : introduction et quelques expériences

En fait, ces deux catégories se basent sur la direction du flux et déterminent la politique.

Le prochain attribut obligatoire est le sélecteur ; celui auquel la règle s'applique. Cela peut être un pod (ou un groupe de pods) ou un environnement (c'est-à-dire un espace de noms). Un détail important : ces deux types d'objets doivent contenir une étiquette (label dans la terminologie Kubernetes) — ce sont celles que les politiques utilisent.

En plus du nombre final de sélecteurs, regroupés par une étiquette donnée, il existe la possibilité d'écrire des règles telles que « autoriser/interdire tout/tous » dans différentes variations. Pour cela, des constructions de ce type sont utilisées :

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

— dans cet exemple, tout le trafic entrant est interdit à tous les pods de l'environnement. Un comportement opposé peut être obtenu avec la construction suivante :

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

De la même manière pour le trafic sortant :

  podSelector: {}
  policyTypes:
  - Egress

— pour le désactiver. Et voici ce qu'il faut pour l'activer :

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

En revenant au choix du plugin CNI pour le cluster, il convient de noter que non tous les plugins réseau ne prennent pas en charge les politiques réseau. Par exemple, le Flannel déjà mentionné ne sait pas configurer les politiques réseau, ce qui est explicitement indiqué dans le dépôt officiel. Une alternative est également mentionnée — un projet Open Source Calico, qui étend considérablement l'ensemble des API standard de Kubernetes en matière de politiques réseau.

Calico pour le réseau dans Kubernetes : introduction et quelques expériences

Découvrons Calico : théorie

Le plugin Calico peut être utilisé en intégration avec Flannel (sous-projet Canal) ou de manière autonome, couvrant à la fois les fonctions de connectivité réseau et les possibilités de gestion d'accessibilité.

Quelles possibilités offre l'utilisation d'une solution « prête à l'emploi » K8s et de l'ensemble des API de Calico ?

Voici ce qui est intégré dans NetworkPolicy :

  • les politiques sont limitées à l'environnement ;
  • les politiques s'appliquent aux pods étiquetés ;
  • les règles peuvent être appliquées aux pods, environnements ou sous-réseaux ;
  • les règles peuvent contenir des protocoles, des indications de ports nommées ou symboliques.

Et voici comment Calico étend ces fonctions :

  • les politiques peuvent s'appliquer à tout objet : pod, conteneur, machine virtuelle ou interface ;
  • les règles peuvent contenir une action spécifique (interdiction, autorisation, journalisation) ;
  • comme cible ou source des règles, il peut y avoir un port, une plage de ports, des protocoles, des attributs HTTP ou ICMP, une adresse IP ou un sous-réseau (IPv4 ou IPv6), et tout sélecteur (nœuds, hôtes, environnements) ;
  • il est également possible de réguler le passage du trafic à l'aide des configurations DNAT et des politiques de transfert de trafic.

Les premiers commits sur GitHub dans le dépôt Calico datent de juillet 2016, et déjà un an plus tard, le projet occupait une position de leader dans l'organisation de la connectivité réseau de Kubernetes — c'est ce que révèlent, par exemple, les résultats d'une enquête, réalisée par The New Stack:

Calico pour le réseau dans Kubernetes : introduction et quelques expériences

De nombreuses solutions managed majeures avec K8s, telles que Amazon EKS, Azure AKS, Google GKE et d'autres, ont commencé à recommander son utilisation.

En ce qui concerne les performances, tout est remarquable. Lors des tests de son produit, l'équipe de développement de Calico a montré des résultats astronomiques, en lançant plus de 50 000 conteneurs sur 500 nœuds physiques à une vitesse de création de 20 conteneurs par seconde. Aucun problème de mise à l'échelle n'a été détecté. Ces résultats ont été annoncés déjà lors de l'annonce de la première version. Des études indépendantes, axées sur la bande passante et les volumes de consommation des ressources, confirment également les performances de Calico, qui ne sont pratiquement pas inférieures à celles de Flannel. Par exemple:

Calico pour le réseau dans Kubernetes : introduction et quelques expériences

Le projet se développe très rapidement, supporte le travail dans les solutions populaires de managed K8s, OpenShift, OpenStack, et permet d'utiliser Calico lors du déploiement d'un cluster avec kops, on trouve des mentions de la construction de réseaux Service Mesh (voici un exemple d'utilisation conjointe avec Istio).

La pratique avec Calico

En règle générale, l'utilisation de Kubernetes vanilla implique l'application d'un fichier calico.yaml, téléchargé depuis le site officiel, à l'aide de kubectl apply -f.

En général, la version actuelle du plugin est compatible avec les 2-3 dernières versions de Kubernetes : le fonctionnement avec des versions plus anciennes n'est pas testé et pas garanti. Selon les déclarations des développeurs, Calico fonctionne sur un noyau Linux supérieur à 3.10 sous CentOS 7, Ubuntu 16 ou Debian 8, au-dessus d'iptables ou d'IPVS.

Isolation au sein de l'environnement

Pour une compréhension générale, examinons un cas simple afin de comprendre comment les politiques réseau en notation Calico diffèrent des standards et comment l'approche de formulation des règles simplifie leur lisibilité et leur flexibilité de configuration :

Calico pour le réseau dans Kubernetes : introduction et quelques expériences

Dans le cluster, deux applications web sont déployées : l'une sur Node.js et l'autre sur PHP, dont l'une utilise Redis. Pour interdire l'accès à Redis depuis PHP tout en maintenant la connectivité avec Node.js, il suffit d'appliquer la politique suivante :

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

En fait, nous avons autorisé le trafic entrant sur le port Redis depuis Node.js. Et nous n'avons explicitement interdit rien d'autre. Dès qu'une NetworkPolicy apparaît, tous les sélecteurs mentionnés commencent à s'isoler, sauf indication contraire. Les règles d'isolement ne s'appliquent pas à d'autres objets non couverts par le sélecteur.

L'exemple utilise apiVersion de Kubernetes « prêt à l'emploi », mais rien n'empêche d'utiliser la ressource homonyme fournie par Calico. La syntaxe y est plus développée, il faudra donc réécrire la règle pour le cas décrit précédemment de la manière suivante :

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

Les constructions mentionnées ci-dessus pour autoriser ou interdire tout le trafic à l'aide de l'API NetworkPolicy classique contiennent des constructions difficiles à comprendre et à mémoriser avec des parenthèses. Dans le cas de Calico, pour inverser la logique de fonctionnement de la règle du pare-feu, il suffit de changer action: Allow sur action: Deny.

Isolation par environnements

Imaginons maintenant une situation où une application génère des métriques commerciales pour leur collecte dans Prometheus et une analyse ultérieure via Grafana. L'export peut contenir des données sensibles, qui, par défaut, sont à nouveau accessibles à tous. Nous allons cacher ces données des regards indiscrets :

Calico pour le réseau dans Kubernetes : introduction et quelques expériences

Prometheus est généralement déployé dans un environnement de service distinct – dans cet exemple, ce sera un namespace de la manière suivante :

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

Champ metadata.labels n'est pas tombé là par hasard. Comme mentionné ci-dessus, namespaceSelector (tout comme podSelector) opère avec des labels. Par conséquent, pour permettre la collecte de métriques depuis tous les pods sur un port spécifique, il sera nécessaire d'ajouter un label quelconque (ou d'en prendre un parmi ceux existants), puis d'appliquer une configuration comme :

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

En cas d'utilisation des politiques Calico, la syntaxe sera la suivante :

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

Dans l'ensemble, en ajoutant ce type de politiques pour des besoins spécifiques, il est possible de se protéger contre les interventions malveillantes ou accidentelles dans le fonctionnement des applications au sein du cluster.

La meilleure pratique, selon les créateurs de Calico, est l'approche « Interdire tout et ouvrir explicitement ce qui est nécessaire », énoncée dans la documentation officielle (d'autres adhèrent à une approche similaire — en particulier, dans l'article déjà mentionné).

L'application d'objets Calico supplémentaires

Je rappelle qu'avec un ensemble API avancé, Calico permet de réguler l'accessibilité des nœuds, sans se limiter aux pods. Dans l'exemple suivant, avec GlobalNetworkPolicy la possibilité de traiter les requêtes ICMP dans le cluster est fermée (par exemple, des pings d'un pod au nœud, entre des pods ou d'un nœud à l'IP d'un 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

Dans le cas ci-dessus, les nœuds du cluster conservent la possibilité de communiquer entre eux via ICMP. Et cette question est résolue par les moyens GlobalNetworkPolicy, appliqués à l'entité 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"]

Le cas du VPN

Enfin, je donnerai un exemple concret d'utilisation des fonctions de Calico pour un cas d'interaction autour du cluster, lorsque l'ensemble standard de politiques n'est pas suffisant. Pour accéder à une application web par les clients, un tunnel VPN est utilisé, et cet accès est strictement contrôlé et limité à une liste spécifique de services autorisés :

Calico pour le réseau dans Kubernetes : introduction et quelques expériences

Les clients se connectent au VPN via le port UDP standard 1194 et reçoivent lors de la connexion des routes vers les sous-réseaux des pods et des services. Les sous-réseaux sont entièrement poussés pour ne pas perdre de services lors des redémarrages et des changements d'adresses.

Le port dans la configuration est standard, ce qui impose certaines nuances au processus de configuration de l'application et de son transfert dans un cluster Kubernetes. Par exemple, dans le même AWS LoadBalancer, le support UDP est apparu littéralement à la fin de l'année dernière dans une liste régionale limitée, et NodePort ne peut pas être utilisé à cause de son routage sur tous les nœuds du cluster, rendant impossible la mise à l'échelle du nombre d'instances du serveur pour des raisons de résilience. De plus, il faudra changer la plage de ports par défaut…

Après avoir exploré les solutions possibles, la suivante a été choisie :

  1. Les pods avec VPN sont prévus sur un nœud en mode hostNetwork, c'est-à-dire sur l'IP réelle.
  2. Le service est exposé à l'extérieur via ClusterIP. Un port est physiquement ouvert sur le nœud, qui est accessible de l'extérieur avec quelques réserves (condition d'avoir une véritable adresse IP).
  3. La définition du nœud sur lequel le pod a été déployé est en dehors de notre propos. Je dirai simplement qu'il est possible de 'fixer' fermement le service à un nœud ou d'écrire un petit service sidecar qui surveillera l'adresse IP actuelle du service VPN et mettra à jour les enregistrements DNS des clients — selon l'imagination de chacun.

En termes de routage, nous pouvons identifier de manière unique un client sur le VPN par son adresse IP fournie par le serveur VPN. Ci-dessous — un exemple simpliste de restriction d'accès à un tel client aux services, illustré sur le Redis mentionné ci-dessus :

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]

Ici, la connexion au port 6379 est strictement interdite, mais le service DNS reste opérationnel, dont le fonctionnement souffre souvent lors de l'élaboration des règles. Car, comme mentionné précédemment, lorsqu'un sélecteur est présent, une politique restrictive par défaut s'applique, sauf indication contraire.

Résultats

Ainsi, grâce à l'API étendue de Calico, il est possible de configurer de manière flexible et de modifier dynamiquement le routage dans le cluster et autour de celui-ci. En général, son utilisation peut ressembler à utiliser un canon pour tuer un moineau, et la mise en place d'un réseau L3 avec des tunnels BGP et IP-IP semble monstrueuse dans une installation simple de Kubernetes sur un réseau plat... Cependant, l'outil demeure entièrement viable et utile.

L'isolation du cluster pour répondre aux exigences de sécurité n'est pas toujours réalisable, et c'est précisément dans ces cas que Calico (ou une solution similaire) entre en jeu. Les exemples donnés dans l'article (avec quelques modifications mineures) sont utilisés dans plusieurs installations de nos clients sur AWS.

P.S.

Lisez aussi dans notre blog :

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster