Integracja argo-rollouts z traffic splittingiem w ISTIO, cz. 3

Sławomir Wolak (Gimbus)
22. maja 2024
Reading time: 13 min
Integracja argo-rollouts z traffic splittingiem w ISTIO, cz. 3

Wracamy do naszego cyklu o ISTIO. Opisując integrację argo-rollouts z traffic splittingiem w ISTIO, w 1 cz. został omówiony temat Argo Rollouts bez ISTIO. w cz. 2 – czas na Argo Rollouts z ISTIO oraz opisanie subset leveli; a wszystko to w formie testów. w Ostatniej części będzie czas na podsumowanie, ale też opisanie Traffic-Splittingu oraz gateway, a wszystko to w formie testów.

Wróćmy do zmian w rolloutach – do tej pory zmienialiśmy jedynie obraz (via kubectl argo rollouts set image test-rollout-istio app07=gimboo/nginx_nonroot3). Sprawdźmy więc, jak to działa ale z podmianą CM, a zatem wprowadźmy nowy rollout zawierający odwołanie do CM + nowy obiekt CM.

rollout-deploy-server-ISTIO-ConfigMap.yaml
config-map-01.yaml 

różnica w kontekście deploymentu (a właściwie oczywiście jego emulacji) jest następująca:

$ diff rollout-deploy-server-ISTIO.yaml rollout-deploy-server-ISTIO-ConfigMap.yaml.optional
35c35,43
<
---
>         volumeMounts:
>         - name: config
>           mountPath: "/config"
>           readOnly: true
>       volumes:
>       - name: config
>         configMap:
>           # Provide the name of the ConfigMap you want to mount.
>           name: cm-01

dodajemy:

kk apply -f  rollout-deploy-server-ISTIO-ConfigMap.yaml
kk apply -f config-map-01.yaml 

Po wgraniu nowej wersji rolloutu (to oczywiście ten sam AR ale z jedną małą zmianą bo ten AR używa od tej pory CM) pojawia sie nowy rollout , zaś jak się wejdzie na PODa to widać zmienne z ConfigMapy:

nginx@test-rollout-istio-8675765c8c-4ffmt:/$ cat /config/key01 ; echo
alamakota
nginx@test-rollout-istio-8675765c8c-4ffmt:/$ cat /config/key02 ; echo
ma
nginx@test-rollout-istio-8675765c8c-4ffmt:/$ cat /config/key03 ; echo
kota  

podmieńmy teraz ale znowu nie obraz ale ConfigMmapę – załadujmy kolejną nową:

apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-02
data:
  key01: "alamakota 2"
  key02: "ma 2 "
  key03: "kota 2"

$ kk apply -f  config-map-02.yaml
configmap/cm-02 created

$ kk get cm
NAME               DATA   AGE
cm-01              3      17m
cm-02              3      6s
kube-root-ca.crt   1      5h        

idąc za dokuentem

Just as with Deployments, any change to the Pod template field (spec.template) results in a new version (i.e. ReplicaSet) to be deployed. * Updating a Rollout involves modifying the rollout spec, typically changing the container image field with a new version, and then running kubectl apply against the new manifest. As a convenience, the rollouts plugin provides a set image command, which performs these steps against the live rollout object in-place*

widać że jakakolwiek modyfikacja AR via set image czy jakiekolwiek zmiany w definicji AR powodują uruchomienie mechanizmu rolloutu 

Jedno co dziwi to jak widać do set image dorobiono CLI (kubectl argo rollouts set image test-rollout-istio app07=gimboo/nginx_nonroot2) a do innych modyfikacji już nie 

Zatem musimy sami sobie zmienić w rollout.yaml wskazanie na inną config-mapę.

 

$ diff rollout-deploy-server-ISTIO-ConfigMap.yaml rollout-deploy-server-ISTIO-ConfigMap-02.yaml
43c43
<           name: cm-01
---
>           name: cm-02

$ kk apply -f  rollout-deploy-server-ISTIO-ConfigMap-02.yaml
rollout.argoproj.io/test-rollout-istio configured

pojawił się nowy POD i pojawił się nowy ROLLOUT  niby mała zmiana (zamontowanie innej CM) a jednak jest zmianą –  więc AR powołało nową revision i wstrzymało ją z wagą na 5% w VS.

$ kubectl argo rollouts get rollout test-rollout-istio
Name:            test-rollout-istio
Namespace:       test-ar-istio
Status:          ॥ Paused
Message:         CanaryPauseStep
Strategy:        Canary
  Step:          1/2
  SetWeight:     5
  ActualWeight:  5
Images:          gimboo/nginx_nonroot (canary, stable)
Replicas:
  Desired:       1
  Current:       2
  Updated:       1
  Ready:         2
  Available:     2

NAME                                            KIND        STATUS        AGE   INFO
⟳ test-rollout-istio                            Rollout     ॥ Paused      5h4m  
├──# revision:12                                                                
│  └──⧉ test-rollout-istio-549bbd66c6           ReplicaSet    Healthy     18s   canary
│     └──□ test-rollout-istio-549bbd66c6-h5km8  Pod           Running     18s   ready:2/2
├──# revision:11                                                                
│  └──⧉ test-rollout-istio-8675765c8c           ReplicaSet    Healthy     21m   stable
│     └──□ test-rollout-istio-8675765c8c-4ffmt  Pod           Running     21m   ready:2/2


$ kk exec -ti test-rollout-istio-8675765c8c-4ffmt -- cat /config/key01 ; echo
alamakota
$ kk exec -ti test-rollout-istio-549bbd66c6-h5km8 -- cat /config/key01 ; echo
alamakota 2 

warto zaznaczyć że w AR da się podmieniać via CLI tylko image – jakiekolwiek inne zmiany trzeba robić ręcznie modyfikując rollout spec

zaś modyfikując AR otrzymujemy nową rewizję AR 

Updating a Rollout involves modifying the rollout spec.

Dodajmy teraz GATEWAY

Wykonuje sie to normalnie , jak w klasyku ISTIO, czyli dodając sekcje gateway do definicji VS:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: virtservice-app07
spec:
  hosts:
  - app07.test-ar-istio.svc.cluster.local
  - uk.bookinfo7.com
  gateways:
  - mesh
  - test-ar-istio/mygateway
  http:
  - route:
    - destination:
        host: app07.test-ar-istio.svc.cluster.local
        subset: version-v2
      weight: 50
    - destination:
        host: app07.test-ar-istio.svc.cluster.local
        subset: version-v1
      weight: 50
    name: primary

Ogólnie można sobie wygodnie porównać zwykłe wdrożenie Traffic-Splittingu via ISTIO-bez-AR z takim gdzie to AR zarządza ISTIO. 

w repo w katalogu AR-z-ISTIO-SubsetLevelTrafficSplitting jest podfolder ISTIO-classic a w nim wszystkie obiekty do wdrożenia drugiego zestawu usług – jak sie wdroży obiekty z niego to mamy stan, gdzie app07 jest oparte o ArgoRollouts a app08 to gołe ISTIO.

 


$ kk get vs
NAME                GATEWAYS                             HOSTS                                                          AGE
virtservice-app07   ["mesh","test-ar-istio/mygateway"]   ["app07.test-ar-istio.svc.cluster.local","uk.bookinfo7.com"]   7h51m
virtservice-app08   ["mesh","test-ar-istio/mygateway"]   ["app08.test-ar-istio.svc.cluster.local","uk.bookinfo8.com"]   36m
$ kk get dr
NAME             HOST                                    AGE
destrule-app07   app07.test-ar-istio.svc.cluster.local   7h52m
destrule-app08   app08.test-ar-istio.svc.cluster.local   37m
$ kk get gateway
NAME        AGE
mygateway   66m

Uwaga, gateway występuje 2 razy, w 2 plikach – są niemal identyczne ale ten drugi uzupełnia GW o dodatkowe hosty dla app08 (wystarczy zrobić diff na tych 2 plikach!

18,19c18,19
<     #- app08.test-ar-istio.svc.cluster.local
<     #- uk.bookinfo8.com
---
>     - app08.test-ar-istio.svc.cluster.local
>     - uk.bookinfo8.com

TESTY:

testy na k8s-svc (wewnątrz klastra) wykonujemy z PODa consumera a testy połączenia via GW z jakiejś stacji na zewnątrz klastra (np z GCE stojącej obok węzłów GKE).


nginx@consumer-58f6fd4c95-gnrmt:/$ curl app07:8080
test-rollout-istio-dd4cc6cb4-lvjw2
<br>2
nginx@consumer-58f6fd4c95-gnrmt:/$ curl app07:8080
test-rollout-istio-dd4cc6cb4-lvjw2
<br>2

$ kk get svc -n istio-system
NAME                   TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)                                      AGE
istio-ingressgateway   LoadBalancer   10.108.1.52    10.128.0.5    15020:32604/TCP,443:30467/TCP,80:30423/TCP   5h45m
istiod                 ClusterIP      10.108.5.9     <none>        15010/TCP,15012/TCP,443/TCP,15014/TCP        5h45m
istiod-asm-1162-2      ClusterIP      10.108.13.34   <none>        15010/TCP,15012/TCP,443/TCP,15014/TCP        5h45m

slawek@instance-1:~$ curl -k --resolve uk.bookinfo7.com:80:10.128.0.5 http://uk.bookinfo7.com
test-rollout-istio-dd4cc6cb4-lvjw2
<br>2
slawek@instance-1:~$

Host-level traffic splitting

Wróćmy do metody omawianej w getting-started, gdzie omówiono nieco inną koncepcję – Host-level Traffic Splitting.

The first approach to traffic splitting using Argo Rollouts and Istio, is splitting between two hostnames, or Kubernetes Services: a canary Service and a stable Service

Folder w tym repo z plikami YAML: AR-z-ISTIO-HostLevel-TrafficSplitting

kubectl create ns test-ar-istio-2
kubectl config set-context --current --namespace=test-ar-istio-2
kubectl -n istio-system get pods -l app=istiod --show-labels | grep rev
kubectl label namespace test-ar-istio-2 istio-injection- istio.io/rev=asm-1162-2 --overwrite

Koncepcja ta bazuje na AR, który definiuje:

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: test-rollout-istio
spec:
  replicas: 1
  strategy:
    canary:
      canaryService: canary-service
      stableService: stable-service
      trafficRouting:
        istio:
          virtualServices:
          - name: virtservice-app07
            routes:
            - primary          
      steps:
      - setWeight: 5
      - pause: {}
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      name: app07
  template:
    metadata:
      labels:
        name: app07
    spec:
      containers:
      - image: gimboo/nginx_nonroot
        name: app07
        resources: {}

W definicji AR jak widać jest wskazanie na dwa k8s-svc (canary-service i stable-service) oraz na virt-service, w wyniku działania AR powstaje 1 POD (lub 2 PODy jesli następuje wdrożenie nowej wersji rolloutu). PODy te mają dodawane rollouts-pod-template-hash, jednocześnie AR modyfikuje nasze k8s-SVC tak że ich selectory poza oryginalnym selector-name-app07 mają również:


  selector:
    name: app07
    rollouts-pod-template-hash: dd4cc6cb4

w chwili gdy jest tylko jedna rewizja rolloutu te SVC wskazują na to samo:


$ kk get po -o wide --show-labels
NAME                                 READY   STATUS    RESTARTS   AGE   IP            NODE                                     NOMINATED NODE   READINESS GATES   LABELS
test-rollout-istio-dd4cc6cb4-bjzkv   2/2     Running   0          22m   10.104.0.23   gke-central-default-pool-6ac74c87-ldjn   <none>           <none>            name=app07,rollouts-pod-template-hash=dd4cc6cb4,security.istio.io/tlsMode=istio,service.istio.io/canonical-name=test-rollout-istio-dd4cc6cb4,service.istio.io/canonical-revision=latest,topology.istio.io/network=a-r-008-default


$ kk get svc
NAME             TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
canary-service   ClusterIP   10.108.8.99    <none>        8080/TCP   43m
stable-service   ClusterIP   10.108.6.132   <none>        8080/TCP   43m

$ kk get svc stable-service -o yaml
[...]
  selector:
    name: app07
    rollouts-pod-template-hash: dd4cc6cb4
[...]

$ kk get svc canary-service -o yaml
[...]
  selector:
    name: app07
    rollouts-pod-template-hash: dd4cc6cb4
[...]

$ kk get ep
NAME             ENDPOINTS          AGE
canary-service   10.104.0.23:8080   43m
stable-service   10.104.0.23:8080   43m

z kolei serce układu czyli VirtService ma 2 destination , każda na inny k8s-svc , na stable-service ma w=100, na canary-service w=0 


$ kk get vs
NAME                GATEWAYS   HOSTS                                                            AGE
virtservice-app07              ["app07-vs","uk.bookinfo7.com"]   42m

$ kk get vs virtservice-app07 -o yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: virtservice-app07
  namespace: test-ar-istio-2
spec:
  hosts:
  - app07-vs
  - uk.bookinfo7.com
  http:
  - name: primary
    route:
    - destination:
        host: stable-service
      weight: 100
    - destination:
        host: canary-service
      weight: 0

zaraz po  wykonaniu modyfikacji:


$ kubectl argo rollouts set image test-rollout-istio app07=gimboo/nginx_nonroot3
rollout "test-rollout-istio" image updated

pojawia się drugi POD z innym rollouts-pod-template-hash:


$ kk get po -o wide --show-labels
NAME                                 READY   STATUS    RESTARTS   AGE   IP            NODE                                     NOMINATED NODE   READINESS GATES   LABELS
test-rollout-istio-5bfbf9599-xl7mv   2/2     Running   0          10s   10.104.1.23   gke-central-default-pool-6ac74c87-w6d2   <none>           <none>            name=app07,rollouts-pod-template-hash=5bfbf9599,security.istio.io/tlsMode=istio,service.istio.io/canonical-name=test-rollout-istio-5bfbf9599,service.istio.io/canonical-revision=latest,topology.istio.io/network=a-r-008-default
test-rollout-istio-dd4cc6cb4-bjzkv   2/2     Running   0          26m   10.104.0.23   gke-central-default-pool-6ac74c87-ldjn   <none>           <none>            name=app07,rollouts-pod-template-hash=dd4cc6cb4,security.istio.io/tlsMode=istio,service.istio.io/canonical-name=test-rollout-istio-dd4cc6cb4,service.istio.io/canonical-revision=latest,topology.istio.io/network=a-r-008-default

k8s-SVC już wyglądają inaczej (tzn stable-service wygląda tak samo, ale canary-service zmienił sie tak że jego selector wyłapuje nowego PODa):


$ kk get svc stable-service -o yaml | grep rollouts-pod-template-hash
    rollouts-pod-template-hash: dd4cc6cb4
$ kk get svc canary-service -o yaml | grep rollouts-pod-template-hash
    rollouts-pod-template-hash: 5bfbf9599

$ kk get ep
NAME             ENDPOINTS          AGE
canary-service   10.104.1.23:8080   49m
stable-service   10.104.0.23:8080   49m

Virt-service zmienił wagi:

$ kk get vs virtservice-app07 -o yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: virtservice-app07
  namespace: test-ar-istio-2
spec:
  hosts:
  - app07-vs
  - uk.bookinfo7.com
  http:
  - name: primary
    route:
    - destination:
        host: stable-service
      weight: 95
    - destination:
        host: canary-service
      weight: 5

Niestety nie do końca wiadomo jak to wszystko przetestować w środku klastra bo mamy 2 różne k8s-svc, a próba łączenia się na hostname z VS kończy się błędem (zresztą próba ta sensu nie miała bo nikt jej w DNS nie założył).


nginx@consumer-58f6fd4c95-fq2r7:/$ curl app07-vs:8080
curl: (6) Could not resolve host: app07-vs  

 

Być może jedyną opcją na dostanie się do usługi (i obejrzenie że scenariusz 95/5 działa) jest dostęp via gateway, trzeba odkomentować sekcje dla gateway w definicji VS:


apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: virtservice-app07
  #namespace: istio-system
spec:
  hosts:
  - app07-vs
  - uk.bookinfo7.com
  gateways:
  - mygateway
  http:
  - route:
    - destination:
        host: stable-service #Should match rollout.spec.strategy.canary.stableService
      weight: 50
    - destination:
        host: canary-service #Should match rollout.spec.strategy.canary.canaryService
      weight: 50
    name: primary # Should match rollout.spec.strategy.canary.trafficRouting.istio.virtualServices.routes

i dodać tenże gateway:


$ kk apply -f EXPOSE_gateway_simple_MATCH_URI_PREFIX.yaml

Faktycznie, wydaje się, że działa:


nginx@consumer-58f6fd4c95-fq2r7:/$ curl -k --resolve uk.bookinfo7.com:80:10.128.0.5 http://uk.bookinfo7.com

No ale, jeśli do VS trzeba dorzucać gateway ażeby z nim porozmawiać, to zaczyna to powoli nie mieć sensu.

ZRÓBMY ZATEM PODSUMOWANIE JAK DZIAŁA Host-level Traffic-Split

  • administrator zamiast 2xDeploy powołuje ROLLOUT w którym:
    • wskazuje na VirtService którym AR będzie kręcił via wstawianie wag (dlatego podaje się tam nazwę route – np „primary”) 
    • wskazuje na 2 x k8s-SVC (podaje tylko ich nazwy)  
    • określa steps dla promocji rolloutu oraz pause{} 
    • emuluje Deploy (wskazuje image, resources itd) 
  • admin powołuje 2 x k8s-SVC ( w selectory tych 2xk8s_svc wstawia coś co będzie wspólne dla 2 wersji PODów – np label=name=app07), potem AR natychmiast i w trybie ciągłym kręci selectorami tych 2 k8s-svc wstawiając im tam rollouts-pod-template-hash)
  • admin powołuje VirtService (definiuje tam 2 x destination, każdy z nich wskazuje na osobny k8s-svc) – tym VS też już za chwilę zarządzał będzie AR  – ale jedynie kręcąc w nim wagami 
  • to nie admin ale oczywiście AR zaczyna zarządzać TraficSplitingiem poprzez ustawianie wag w VirtService – 100/0 i 95/5 ORAZ poprzez modyfikacje selectorów w 2 x k8s-svc


To samo podejście jest prezentowane tu – podsumowano zresztą dosyć analitycznie cały mechanizm host-level-traffic-splitting:

During the lifecycle of a Rollout update, Argo Rollouts will continuously:

modify the canary Service spec.selector to contain the rollouts-pod-template-hash label of the canary ReplicaSet modify the stable Service spec.selector to contain the rollouts-pod-template-hash label of the stable ReplicaSet modify the VirtualService spec.http[].route[].weight to match the current desired canary weight.

Subset-level Traffic Splitting vs Host-level Traffic Splitting

Podejście Subset-level Traffic Splitting wydaje się być ISTIO-way i jest naturalnym powieleniem dotychczasowych mechanizmów dla ważenia ruchu w ISTIO. 

Podejście Host-level Traffic Splitting w wydaje się być nienaturalne i uciążliwe w próbie nałożenia go na klasyczne schematy ISTIO , dodatkowo nie da się łatwo korzystać z serwisów bez użycia gatewaya, deploymenty chcące łączyć sie wprost do canary/stable nie będą mogły zrobić tego naraz tylko muszą sobie wybrać do której konkretnie wersji chcą się łączyć, gdy poczyta się co nieco na ten temat, to okazuje się, że początkowo AR-ISTIO bazowały wyłącznie na Host-level Traffic Splitting, które było krytykowane, po jakimś czasie pojawiło się podejście oparte o Subset-level Traffic Splitting – dużo naturalniejsze i łatwe do zmapowania ze zwykłego Traffic Splittingu, opartego na ISTIO –

bardzo trafnie oceniono to tutaj (maj 2020): argoproj/argo-rollouts#617

Currently, according to https://argoproj.github.io/argo-rollouts/features/traffic-management/istio/, Istio canary deployments require separate services for stable and canary. This is undesirable, as I understand in-mesh communications won’t hit the virtualservice directly (there’s no DNS entry added by default for those in the Kubernetes cluster). This forces microservice-to-microservice communication to choose whether to hit the stable service or the canary (hardcoded), or go to the gateway, or DNS entries to be added for the virtualservices, which is cumbersome and poorly documented.

All Istio canary examples I could find use a single service, but have a destinationrule that splits them into 2 groups based on label selectors (for canary vs stable, or per version), then the virtualservice links this single service and splits the traffic accordingly.

Finalnie, na prośbę community w okolicach Q1 2021, dodano obsługę Subset-level Traffic Splitting, pozostawiając również na prośbę niezbyt udane Host-level Traffic Splitting argoproj/argo-rollouts#985.