Calico do sieci w Kubernetes: wprowadzenie i odrobina doświadczeń

Calico do sieci w Kubernetes: wprowadzenie i odrobina doświadczeń

Celem artykułu jest zapoznanie czytelnika z podstawami komunikacji sieciowej oraz zarządzania politykami sieciowymi w Kubernetes, a także z zewnętrznym wtyczką Calico, która rozszerza standardowe możliwości. W ramach artykułu zaprezentowane zostaną wygoda konfiguracji oraz niektóre funkcje na rzeczywistych przykładach z naszego doświadczenia.

Szybkie wprowadzenie do urządzenia sieciowego Kubernetes

Klastrowi Kubernetes nie można sobie wyobrazić bez sieci. Publikowaliśmy już materiały na ten temat: „Ilustrowane przewodnik po strukturze sieci w Kubernetes” i „Wprowadzenie do polityk sieciowych Kubernetes dla specjalistów ds. bezpieczeństwa.».

W kontekście tego artykułu ważne jest zaznaczenie, że za połączenie sieciowe między kontenerami i węzłami nie odpowiada sam K8s: do tego używa się różnorodnych wtyczek CNI (Container Networking Interface). Więcej na ten temat mówiliśmy także.

Na przykład najczęściej używaną z takich wtyczek — Flannel — zapewnia pełną komunikację sieciową między wszystkimi węzłami klastra poprzez tworzenie mostów na każdym węźle, przypisując mu podsieć. Jednak pełna i nieograniczona dostępność nie zawsze jest korzystna. Aby zapewnić jakąś minimalną izolację w klastrze, konieczne jest ingerowanie w konfigurację zapory. W ogólnym przypadku jest ona zarządzana przez same CNI, przez co jakiekolwiek zewnętrzne ingerencje w iptables mogą być interpretowane niepoprawnie lub całkowicie ignorowane.

A „z pudełka” do organizacji zarządzania politykami sieciowymi w klastrze Kubernetes dostarczane jest NetworkPolicy API. Ten zasób, rozprzestrzeniający się na wybrane przestrzenie nazw, może zawierać reguły do rozdzielania dostępu między różnymi aplikacjami. Pozwala to również skonfigurować dostępność między konkretnymi podami, środowiskami (przestrzeniami nazw) lub blokami adresów 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

To nie jest najbardziej prymitywny przykład z oficjalnej dokumentacji może raz na zawsze odstraszyć od chęci zgłębiania logiki działania polityk sieciowych. Niemniej jednak spróbujemy zrozumieć podstawowe zasady i metody przetwarzania ruchu sieciowego z wykorzystaniem polityk sieciowych…

Logiczne jest, że istnieją 2 typy ruchu: przychodzący do pod (Ingress) i wychodzący z niego (Egress).

Calico do sieci w Kubernetes: wprowadzenie i odrobina doświadczeń

Właściwie na te 2 kategorie według kierunku ruchu dzieli się polityka.

Kolejny obowiązkowy atrybut to selektor; ten, do kogo stosuje się zasady. Może to być pod (lub grupa podów) lub środowisko (czyli przestrzeń nazw). Ważny szczegół: oba te rodzaje obiektów muszą zawierać etykietę (label w terminologii Kubernetes) — to nimi operują polityki.

Oprócz ograniczonej liczby selektorów, połączonych jakąś etykietą, istnieje możliwość napisania zasad typu „Zezwól/zabronić wszystko/wszystkim” w różnych wariacjach. W tym celu używa się konstrukcji typu:

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

— w tym przykładzie wszystkim podom w środowisku blokowany jest przychodzący ruch. Odwrotne zachowanie można uzyskać przy pomocy takiej konstrukcji:

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

Podobnie dla wychodzącego:

  podSelector: {}
  policyTypes:
  - Egress

— aby go wyłączyć. A oto, co należy zrobić, aby go włączyć:

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

Wracając do wyboru wtyczki CNI dla klastra, warto zauważyć, że nie każda wtyczka sieciowa obsługuje pracę z NetworkPolicy. Na przykład wspomniany już Flannel nie potrafi konfigurować polityk sieciowych, co jest jasno określone w oficjalnym repozytorium. Tam też wspomniana jest alternatywa — projekt Open Source, Calicoktóry znacznie rozszerza standardowy zestaw API Kubernetes w zakresie polityk sieciowych.

Calico do sieci w Kubernetes: wprowadzenie i odrobina doświadczeń

Poznajmy Calico: teoria

Wtyczka Calico może być używana w integracji z Flannel (podprojekt Canal) lub samodzielnie, obejmując zarówno funkcje zapewnienia łączności sieciowej, jak i zarządzania dostępnością.

Jakie możliwości daje użycie „pudełkowego” rozwiązania K8s i zestawu API z Calico?

Oto co jest wbudowane w NetworkPolicy:

  • polityki ograniczone do środowiska;
  • polityki stosowane do podów oznaczonych etykietami;
  • zasady mogą być stosowane do podów, środowisk lub podsieci;
  • zasady mogą zawierać protokoły, nazwane lub symboliczne oznaczenia portów.

A oto jak Calico rozszerza te funkcje:

  • polityki mogą być stosowane do każdego obiektu: pod, kontener, maszyna wirtualna lub interfejs;
  • reguły mogą zawierać konkretne działanie (zakaz, zezwolenie, logowanie);
  • jako cel lub źródło reguł może być port, zakres portów, protokoły, atrybuty HTTP lub ICMP, IP lub podsieć (4 lub 6 generacji), dowolne selektory (węzłów, hostów, środowisk);
  • dodatkowo można regulować przepływ ruchu za pomocą ustawień DNAT i polityk przekierowywania ruchu.

Pierwsze commity na GitHubie w repozytorium Calico pochodzą z lipca 2016 roku, a już rok później projekt zajął wiodące pozycje w organizacji sieciowej Kubernetes — tak mówią na przykład wyniki ankiety, przeprowadzonej przez The New Stack.:

Calico do sieci w Kubernetes: wprowadzenie i odrobina doświadczeń

Wiele dużych rozwiązań managed z K8s, takich jak Amazon EKS, Azure AKS,, Google GKE, i inne, zaczęło go rekomendować do użycia.

Jeśli chodzi o wydajność, to sprawy mają się znakomicie. Podczas testowania swojego produktu zespół deweloperski Calico wykazał astronomiczne wyniki, uruchamiając ponad 50000 kontenerów na 500 fizycznych węzłach z prędkością tworzenia 20 kontenerów na sekundę. Problemy przy skalowaniu nie zostały wykryte. Takie wyniki zostały ogłoszone już przy ogłoszeniu pierwszej wersji. Niezależne badania dotyczące przepustowości i zużycia zasobów również potwierdzają wydajność Calico, która praktycznie nie ustępuje Flannel. Na przykład:

Calico do sieci w Kubernetes: wprowadzenie i odrobina doświadczeń

Projekt rozwija się bardzo szybko, wspiera pracę w popularnych rozwiązaniach managed K8s, OpenShift, OpenStack, istnieje możliwość użycia Calico podczas wdrażania klastra za pomocą kops, pojawiają się wzmianki o budowie sieci Service Mesh (oto przykład użycia wspólnie z Istio).

Praktyka z Calico

W ogólnym przypadku użycia vanilowego Kubernetes instalacja CNI sprowadza się do zastosowania pliku calico.yaml, pobranego z oficjalnej strony., za pomocą kubectl apply -f.

Zazwyczaj aktualna wersja wtyczki jest kompatybilna z 2-3 ostatnimi wersjami Kubernetes: praca w starszych wersjach nie jest testowana i nie jest gwarantowana. Według zapewnień deweloperów, Calico działa na jądrze Linux powyżej 3.10 na CentOS 7, Ubuntu 16 lub Debian 8, na iptables lub IPVS.

Izolacja wewnątrz środowiska.

Aby ogólnie zrozumieć, rozważmy prosty przypadek, aby zobaczyć, czym różnią się polityki sieciowe w notacji Calico od standardowych oraz jak podejście do tworzenia reguł ułatwia ich czytelność i elastyczność konfiguracji:

Calico do sieci w Kubernetes: wprowadzenie i odrobina doświadczeń

W klastrze wdrożono 2 aplikacje webowe: jedną na Node.js i drugą na PHP, z których jedna używa Redis. Aby zablokować dostęp do Redis z PHP, jednocześnie zachowując połączenie z Node.js, wystarczy zastosować następującą politykę:

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

Zasadniczo zezwoliliśmy na ruch przychodzący na port Redis z Node.js. I wyraźnie niczego innego nie zabranialiśmy. Gdy tylko pojawia się NetworkPolicy, wszystkie selektory w niej wymienione zaczynają się izolować, chyba że wskazano inaczej. Reguły izolacji nie rozciągają się na inne obiekty, które nie są objęte selektorem.

W przykładzie użyto apiVersion Kubernetes’a „z pudełka”, ale nic nie stoi na przeszkodzie, aby używać jednoimiennego zasobu z dostawy Calico. Składnia jest tam bardziej rozbudowana, więc będzie trzeba przepisać regułę dla wyżej opisanego przypadku w następujący sposób:

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

Wspomniane powyżej konstrukcje do zezwolenia lub zablokowania całego ruchu za pomocą zwykłego API NetworkPolicy zawierają trudne do zrozumienia i zapamiętania konstrukcje ze ścisłymi nawiasami. W przypadku Calico, aby zmienić logikę działania reguły zapory na przeciwną, wystarczy zmienić action: Allow na action: Deny.

Izolacja według środowisk

Teraz wyobraźmy sobie sytuację, kiedy aplikacja generuje dane biznesowe do ich zbierania w Prometheus i dalszej analizy za pomocą Grafana. W zrzucie mogą znajdować się wrażliwe dane, które z założenia są dostępne do ogólnego wglądu. Zablokujmy te dane przed wścibskimi oczami:

Calico do sieci w Kubernetes: wprowadzenie i odrobina doświadczeń

Prometheus jest zazwyczaj umieszczany w oddzielnym środowisku roboczym — w naszym przykładzie będzie to namespace o następującym kształcie:

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

Pole metadata.labels to nie było przypadkowe. Jak już wcześniej wspomniano, namespaceSelector (tak jak i podSelector) operuje etykietami. Dlatego, aby pozwolić na zbieranie metryk z wszystkich podów na określonym porcie, będziesz musiał dodać jakąś etykietę (lub wziąć z istniejących), a następnie zastosować konfigurację podobną do:

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

A w przypadku używania polityk Calico składnia będzie następująca:

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

Ogólnie rzecz biorąc, dodając tego rodzaju polityki pod konkretne potrzeby, można zabezpieczyć aplikacje w klastrze przed złośliwym lub przypadkowym naruszeniem.

Najlepszą praktyką według twórców Calico jest podejście „Zabroń wszystkiego i wyraźnie otwieraj to, co niezbędne”, zapisane w oficjalnej dokumentacji (analogiczne podejście stosują także inni — w szczególności w już wspomnianym artykule).

Zastosowanie dodatkowych obiektów Calico

Przypominam, że dzięki rozszerzonemu zestawowi API Calico można regulować dostępność węzłów, nie ograniczając się tylko do podów. W następnym przykładzie z użyciem GlobalNetworkPolicy zamykana jest możliwość przechodzenia zapytań ICMP w klastrze (na przykład, pingi z poda do węzła, między podami lub z węzła do IP poda):

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

W powyższym przypadku węzły klastra mogą „dostać się” do siebie nawzajem przez ICMP. A ten problem rozwiązuje się za pomocą GlobalNetworkPolicy, zastosowanej do podmiotu 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"]

Przypadek z VPN

Na końcu podam całkiem realny przykład wykorzystania funkcji Calico w przypadku interakcji wokół klastra, gdy standardowy zestaw polityk nie wystarcza. Do dostępu do aplikacji webowej przez klientów używany jest tunel VPN, a dostęp ten jest ściśle kontrolowany i ograniczony określoną listą dozwolonych usług:

Calico do sieci w Kubernetes: wprowadzenie i odrobina doświadczeń

Klienci łączą się z VPN przez standardowy port UDP 1194 i podczas łączenia otrzymują trasy do klastra podsieci i usług. Podsieci są pushowane w całości, aby nie tracić usług podczas ponownych uruchomień i zmiany adresów.

Port w konfiguracji jest standardowy, co nakłada pewne ograniczenia na proces konfigurowania aplikacji i jej przenoszenia do klastra Kubernetes. Na przykład w AWS LoadBalancer port UDP pojawił się zaledwie pod koniec zeszłego roku w ograniczonej liczbie regionów, a NodePort nie może być używany z powodu jego przekierowania na wszystkich węzłach klastra i niemożności skalowania liczby instancji serwera dla zapewnienia odporności na awarie. Poza tym, konieczne będzie zmienienie zakresu portów wybieranego domyślnie...

W wyniku analizy możliwych rozwiązań wybrano następujące:

  1. Pod’y z VPN są planowane na węzeł w trybie hostNetwork, czyli na rzeczywisty adres IP.
  2. Usługa jest wystawiana na zewnątrz przez ClusterIP. Na węźle fizycznie uruchamiany jest port, który jest dostępny z zewnątrz z pewnymi zastrzeżeniami (warunkowe posiadanie rzeczywistego adresu IP).
  3. Określenie węzła, na którym uruchomiono pod, leży poza zakresem naszej opowieści. Powiem tylko, że można 'przywiązać' usługę do węzła lub napisać małą usługę sidecar, która będzie monitorować bieżący adres IP usługi VPN i aktualizować rekordy DNS zapisane u klientów — kto jaką ma wyobraźnię.

Z punktu widzenia routingu możemy jednoznacznie zidentyfikować klienta za VPN po jego adresie IP przydzielanym przez serwer VPN. Poniżej znajdują się prosty przykład ograniczenia dostępu dla takiego klienta do usług, ilustracja na wspomnianym wcześniej 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]

Tutaj trwałe połączenie z portem 6379 jest zabronione, ale jednocześnie zachowano działanie usługi DNS, której funkcjonowanie często cierpi podczas tworzenia reguł. Ponieważ, jak wcześniej wspomniano, podczas pojawienia się selektora stosuje się domyślną politykę zakazu, jeśli nie wskazano inaczej.

Podsumowanie

Dzięki rozszerzonemu API Calico można elastycznie konfigurować i dynamicznie zmieniać routowanie w klastrze i wokół niego. W ogólnym przypadku jego użycie może przypominać strzelanie z armaty do wróbli, a wdrożenie sieci L3 z tunelami BGP i IP-IP wydaje się monstrualne w prostej instalacji Kubernetes w płaskiej sieci… Jednak w pozostałych aspektach narzędzie wygląda na całkiem życia zdolne i użyteczne.

Izolacja klastra w celu spełnienia wymagań bezpieczeństwa nie zawsze może być realizowana, i właśnie w takich przypadkach z pomocą przychodzi Calico (lub podobne rozwiązanie). Przykłady przedstawione w artykule (po niewielkich poprawkach) są używane w kilku instalacjach naszych klientów na AWS.

P.S.

Przeczytaj także na naszym blogu:

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster