Izolacja namespaces za pomocą Istio

Izolacja namespaces za pomocą Istio

Jakiś czas temu potrzebowałem odseparować od siebie trzy instancje aplikacji uruchamianej na jednym klastrze. W tym przypadku chodziło o OpenShift, ale nie to jest najważniejsze. Miałem do dyspozycji Istio. Każda z instancji aplikacji byłą zainstalowana w osobnej przestrzeni nazw klastra. Pokażę więc jak wygląda izolacja namespaces za pomocą Istio.

Klaster i instlacja Istio

Na nasze potrzeby utworzyłem klaster Kubernetes w usłudze GKE na platformie Google. Ale nie ma to większego znaczenia. Wszystko powinno zadziałać w każdym klastrze.

Instalację Istio wykonałem za pomocą narzędzie Istioctl. Ale znów nie to jest najważniejsze. Ważne jest to, że opcję meshConfig.outboundTrafficPolicy.mode należy ustawić na wartość REGISTRY_ONLY. Domyślne ustawienie to ALLOW_ANY i w takim przypadku izolacja nie zadziała.

Zainstalowałem więc Istio za pomocą polecenia istioctl install --set meshConfig.outboundTrafficPolicy.mode=REGISTRY_ONLY, a następnie przeszedłem do tworzenia namespaces dla poszczególnych instancji aplikacji.

Namespaces

Na nasze potrzeby utworzyłem dwie przestrzenie nazw. Prod oraz dev. Może nie są to najszczęśliwsze nazwy do testów, ale robiłem to na czystym klastrze, więc nie było problemu z zepsuciem pracujących systemów produkcyjnych.

kubectl create ns prod

kubectl create ns dev

W kolejnym kroku dodałem do namespaces labelki, które spowodują, że do każdego deploymentu „wrzucanego” do nich będzie wstrzykiwane Istio.

kubectl label ns prod istio-injection=enabled

kubectl label ns dev istio-injection=enabled

Mamy przygotowane środowisko, czas na uruchomienie aplikacji.

Deployments

Wszystkie pliki dostępne są oczywiście w repozytorium na GitHub-ie.

Jako uruchamiane aplikacje wykorzystamy prosty serwer, który potrafi między innymi zwracać adres IP oraz nazwę hosta, na którym został uruchomiony. Pełny opis możecie znaleźć tutaj.

W obu namaspaces zrobimy taki sam deployment. Poniżej przykład dla prod.

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: prod
  name: app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
    spec:
      containers:
      - 
        image: przemekmalak/multitool
        name: server
        ports:
        - containerPort: 8080
apiVersion: v1
kind: Service
metadata:
  namespace: prod
  name: app
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: app

Całość jest dostępna w pliku deployments.yaml. wykonujemy więc polecenie kubectl create -f deployments.yaml lub kubectl apply -f https://raw.githubusercontent.com/PrzemekMalak/istio_namespaces_isolation/master/deployments.yaml jeżeli chcecie wykonać deployment bezpośrednio z GitHub.

Sprawdźmy czy wszystko się zainstalowało kubectl get pods -l app=app -A

I jeżeli wszystko jest w porządku, możemy przejść do testowania ruchu.

W tym celu wykorzystamy kontener, w którym mamy dostępne kilka narzędzi sieciowych, między innymi curl.

Uruchommy więc takiego poda w przestrzeni prod, który po zakończeniu pracy zostanie usunięty:

kubectl run test --image=przemekmalak/tools -it --rm -n prod -- sh

 

Wewnątrz wykonajmy dwa polecenia i sprawdźmy dostępność do serwisów w obu namespaces

curl app.prod.svc
curl app.dev.svc

Jak widać mamy dostęp do obu. Nic nie blokuje ruchu.

Wyjdźmy z Poda za pomocą polecenia exit

Pod powinien zostać usunięty z klastra.

Możecie jeszcze sprawdzić to samo dla namespace dev uruchamiając testowego poda za pomocą polecenia

kubectl run test –image=przemekmalak/tools -it –rm -n dev — sh

ale wynik będzie zapewne taki sam.

Wszystko może rozmawiać ze wszystkim. A nam chodzi o to, żeby odseparować poszczególne namespaces.

Izolacja namespaces za pomocą Istio

Jednym z zasobów dostępnych w Istio jest Sidecar. Za jego pomocą możemy miedzy innymi skonfigurować zachowanie proxy, które jest „wstrzykiwane” do każdego Poda. My zajmiemy się ograniczaniem ruchu wychodzącego z każdego Poda.

Ważne są trzy rzeczy, o których wspomina dokumentacja.

1. Każda przestrzeń nazw może mieć tylko jedną konfigurację Sidecara bez ustawionego workloadSelectora. Taka konfiguracja będzie domyślna dla wszystkich podów w tej namespace.

2. Konfiguracja Sidecara w przestrzeni głównej (root) będzie dziedziczona przez wszystkie przestrzenie bez własnej konfiguracji. Nie powinna ona mieć ustawionego workloadSelectora.

3. Przy wybierani konfiguracji priorytet będzie miała ta z ustawionym workloadSelectora.

No to na początek spróbujmy dodać taką konfigurację, która ograniczy ruch wychodzący tylko do Istio oraz do namespace, z którego ruch wychodzi. Konfiguracja będzie dodana do głównej przestrzeni nazw, więc będzie obowiązaywała dla każdej innej. Konfiguracja wygląda tak

apiVersion: networking.istio.io/v1beta1
kind: Sidecar
metadata:
  name: default
  namespace: istio-system
spec:
  egress:
  - hosts:
    - "./*"
    - "istio-system/*"

i możemy dodać ją do klastra poleceniem

kubectl apply -f sidecar-default.yaml lub kubectl apply -f https://raw.githubusercontent.com/PrzemekMalak/istio_namespaces_isolation/master/sidecar-default.yaml

 

Przetestujmy działanie naszym Podem testowym w przestrzeni Prod

kubectl run test --image=przemekmalak/tools -it --rm -n prod -- sh

i po uruchomieniu, wewnątrz Poda

curl app.prod.svc
curl app.dev.svc

 

Tym razem zachowanie będzie inne. Pod z namespace Prod będzie mógł się komunikowac tylko z własną namespace.

Wychodzimy z Poda: exit i uruchamiamy to samo w namespace dev

kubectl run test –-image=przemekmalak/tools -it –-rm -n dev -- sh

Ponownie testy

curl app.prod.svc
curl app.dev.svc

i znów, Pod z namespace Dev komunikuje się tylko z serwisem uruchomionym w tej samej przestrzeni nazw

i wychodzimy z Poda: exit

Działa więc kasujemy naszą konfigurację

kubectl delete -f sidecar-default.yaml

i przechodzimy do innych testów.

Tym razem każda z namespaces otrzyma własną konfigurację Sidecar. Dla przestrzeni Prod wygląda to tak

apiVersion: networking.istio.io/v1beta1
kind: Sidecar
metadata:
  name: prod-only
  namespace: prod
spec:
  egress:
  - hosts:
    - "./*"
    - "istio-system/*"

i dla Dev

apiVersion: networking.istio.io/v1beta1
kind: Sidecar
metadata:
  name:dev-only
  namespace: dev
spec:
  egress:
  - hosts:
    - "./*"
    - "istio-system/*"

W naszym przypadku skutek powinien być taki sam. Różnica jest taka, że powyższe konfiguracje nie będą dotyczyły innych przestrzeni.

Aplikujemy konfiguracje za pomocą polecenia kubectl apply -f sidecar-namespaces.yaml lub https://raw.githubusercontent.com/PrzemekMalak/istio_namespaces_isolation/master/sidecar-namespaces.yaml i ponownie przeprowadzamy testy:

kubectl run test –-image=przemekmalak/tools -it –-rm -n prod -- sh

i wewnątrz poda ponownie

curl app.prod.svc
curl app.dev.svc

Po wyjściu: exit możemy przetestować Poda uruchomionego w namespace Dev. Zachowa się on jednak bardzo podobnie. Będzie miał udostępniony ruch tylko do serwisu app.dev.svc, a nie będzie mógł dostać się do serwisów uruchomionych w innych namespaces.

A co gdy… Wyjątki

No właśnie. Czasami chcemy dopuścić jednak ruch do konkretnego serwisu w innej przestrzeni nazw. Da się? Da się.

Definicja będzie trochę bardziej skomplikowana, ale możliwa. Tak to wygląda w naszym przypadku.

apiVersion: networking.istio.io/v1beta1
kind: Sidecar
metadata:
  name: permit-to-prod
  namespace: dev
spec:
  egress:
  - hosts:
    - "./*"
    - "prod/app.prod.svc.cluster.local"
    - "istio-system/*"
  workloadSelector:
    labels:
      app: allowed-to-prod

Poza umożliwieniem ruchu do namespace istio i do własnej, umożliwiamy ruch wychodzący do konkretnego serwisu w konkretnej przestrzeni nazw.

Aplikujemy więc plik z nowymi zasadami

kubectl apply -f sidecar-permit-to-prod.yaml

I to czego teraz potrzebujemy, to Pod za pomocą którego sprawdzimy nowe możliwości.

apiVersion: v1
kind: Pod
metadata:
  name: test-ok
  namespace: dev
  labels:
    app: allowed-to-prod
spec:
  containers:
  - name: test
    image: przemekmalak/tools
    command: ["/bin/sh", "-ec", "while :; do echo '.'; sleep 5 ; done"]

Wrzućmy go więc na klaster

kubectl apply -f pod-to-prod.yaml

i jeżeli wszystko jest w porządku

wejdźmy do niego

kubectl exec test-ok -it -n dev -- sh

i spróbujmy połączyć się do serwisu w namespace prod.

curl app.prod.svc

Powinno się udać.

Dla pewności uruchommy w namespace dev jeszcze raz zwykłego poda bez labelki app ustawionej na wartość allowed-to-prod i sprawdźmy czy ruch do namespace prod jest zablokowany:

kubectl run test --image=przemekmalak/tools -it --rm -n dev — sh

I ponownie wewnątrz poda spróbujmy się podłączyć do serwisu w namespace prod

curl app.prod.svc

I tym razem nie powinniśmy otrzymać żadnej odpowiedzi.

Mamy więc w tym momencie dwa odseparowane od siebie środowiska, które umożliwiają jednak w jednym przypadku ruch do konkretnego serwisu w innej przestrzeni nazw.

 

Comments are closed.