Kubernetes, canary deployment i Istio
Nie ważne czy wdrażasz aplikacje monolityczne czy mikroserwisy. Na pewno przed wypuszczeniem nowej wersji chciałbyś ją przetestować na jakiejś próbce swoich użytkowników. Powiedzmy na 10 procentach. Na czystym Kubernetesie będzie ciężko. Możesz zarządzać liczbą podów w konkretnym deploymencie. Pody podpinasz pod serwis. I to chyba tyle. Pokażę jak zrobić canary deployment w Kubernetes za pomocą Istio.
Deployment
W przykładzie będę używał swojego obrazu dockerowego. Serwis w nim zawarty potrafi między innymi zwracać tekst zdefiniowany w zmiennej środowiskowej RETURN_TEXT. W ten sposób będę mógł łatwo rozróżnić „wersję” aplikacji. Pierwsza z nich będzie zwracała tekst service-1a
Stworzę dwa deploymenty z dwoma wersjami serwisu. Drugi będzie zwracał tekst service-1b
Na początek wrzucimy na klaster nasze zasoby. Dodatkowo tworzymy namespace, które oznaczamy labelką istio-injection: enabled.
apiVersion: v1
kind: Namespace
metadata:
name: srv
labels:
istio-injection: enabled
To spowoduje, że do wszystkich podów deployowanych w tej przestrzeni będą automatycznie wstrzykiwane komponenty Istio.
Pierwszy deployment wygląda następująco:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: srv
labels:
app: mt1
name: mt1
spec:
replicas: 2
selector:
matchLabels:
app: mt1
ver: "1"
template:
metadata:
labels:
app: mt1
ver: "1"
spec:
containers:
- image: przemekmalak/multitool
name: multitool
env:
- name: RETURN_TEXT
value: service-1a
W drugim zmienimy tylko wartość RETURN_TEXT na service-1b oraz labelkę opisującą wersję na 2
selector:
matchLabels:
app: mt1
ver: "2"
template:
metadata:
labels:
app: mt1
ver: "2"
Serwis
Serwis będzie szukał wszystkich podów spełniających warunek:
selector:
app: mt1
W ten sposób, oba deploymenty „ubierzemy” w jeden serwis. Ruch będzie rozrzucany pomiędzy obie wersje.
Cały plik yaml z definicją serwisu wygląda tak:
apiVersion: v1
kind: Service
metadata:
namespace: srv
labels:
app: mt1
name: mt1
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 8080
selector:
app: mt1
Testy
Jeżeli wszystko już mamy w naszym klastrze gotowe możemy przetestować jak wygląda dystrybucja ruchu. W tym celu uruchomię nowego poda, w którym będę miał curla.
kubectl run test --image=przemekmalak/tools --rm -it -- sh
i z wewnątrz tego poda spróbuję kilka razy podłączyć się do naszego serwisu:
for i in `seq 1 10`
do
curl -m 3 mt1.srv:8080
done
Jak zapewnie zauważyliście ruch został rozłożony po wszystkich podach z obu deploymentów. Ale, tak jak pisałem, ilość requestów przekazywanych do poszczególnych wersji możemy regulować za pomocą ilości podów w poszczególnych deploymentach.
Istio
Zakładam, że samo Istio masz zainstalowane. Jeżeli nie to popatrz tutaj jak to zrobić. To naprawdę tylko kilka kroków. Zainstaluj z profilem demo. To pozwoli potem na zabawę z kilkoma innymi usługami.
Istio dodaje do klastra kilka własnych obiektów, które pozwalają na zarządzanie ruchem. Jednym z kich jest DestinationRule. Za pomocą tego obiektu zdefiniujemy sobie tak zwane subsety. Dwa. Po jednym dla każdej wersji naszej aplikacji. Znowu skorzystamy z labelek w poszczególnych deploymentach.
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
namespace: srv
name: mt1
spec:
host: mt1.srv.svc.cluster.local
subsets:
- name: mt1
labels:
app: mt1
ver: "1"
- name: mt2
labels:
app: mt1
ver: "2"
W Destination rule definiujemy między innymi to, co ma się stać z ruchem, który zostanie skierowany w konkretne miejsce. A w te miejsca ruch będzie kierował kolejny obiekt Istio, czyli Virtual Service. To właśnie tutaj zdefiniujemy zasady routingu. Nasze nie są zbyt skomplikowane, mamy tylko jedną ścieżkę. Ale za jego pomocą możemy także zdefiniować jaka część ruchu ma być przesyłana do poszczególnych wersji. W naszym przypadku, prześlemy 80% ruchu do wersji pierwszej i 20% do wersji drugiej.
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: mt1
namespace: srv
spec:
hosts:
- mt1.srv.svc.cluster.local
http:
- match:
- uri:
prefix: /
route:
- destination:
host: mt1.srv.svc.cluster.local
subset: mt1
port:
number: 8080
weight: 80
- destination:
host: mt1.srv.svc.cluster.local
subset: mt2
port:
number: 8080
weight: 20
Dodajemy oba obiekty do klastra i spróbujmy ponownie sprawdzić jak kierowany jest ruch. Tym razem już przez Istio. Ponownie tworzymy dodatkowego poda kubectl run test --image=przemekmalak/tools --rm -it -n srv -- sh
Zwróćcie uwagę, że tworzę go w przestrzeni nazw srv. I ponownie, wewnątrz poda wykonujemy:
for i in `seq 1 10`
do
curl -m 3 mt1.srv.svc.cluster.local
done
I w efekcie powinniśmy dostać coś zbliżonego do
20% ruchu poszło do wersji 2, a 80% do wersji pierwszej.
Dla leniwych
Dołączam dwa pliki, które możecie wykorzystać. Po rozpakowaniu wystarczy wykonać dwa polecenia: kubectl apply -f 1_services.yaml
oraz kubectl apply -f 2_virtual_services.yaml
Po chwili na klastrze powinniście mieć nasze serwisy i dodatkowy pod o nazwie debug , z którego możecie sprawdzić jak rozrzucany jest ruch.