
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.