Translate

четвер, 9 жовтня 2025 р.

Argo Rollouts. Part II: Canary and PingPong Strategy In Kubernetes

Нещодавно я опублікував першу статтю про Argo Rollouts, де зупинився на прогресивних методах доставки та реалізацію BlueGreen із ALB балансувальником та без. Цього ж разу поговоримо про Canary та його імплементацію в Argo Rollouts.

Ця стаття потребує робочого EKS кластеру, AWS LB контролера і самого Argo Rollouts. Про все це можна почитати в попередніх статтях блогу, а останній Terraform-код знаходиться за наступною адресою.


1. CANARY W/O BALANCER

Офіційна документація пропонує різні варіанти, наприклад коли додаток виливається без сервіс-об'єктів взагалі, чого може бути достатньо для якихось воркерів, що працюють зі сторонньою базою та не мають входу через єдиний ендпоїнт/балансувальник. Чи варіант, коли група подів заводиться лише під один об'єкт сервісу, адже першим етапом такої виливки вже є включення відсотку нової версії коду в трафік. Існує також позиція, що першим етапом Canary має бути под/вузол нової версії, котрий ще не буде під трафіком, але його можна буде обкласти додатковими тестами. Власне варіацій багато, єдиного стандарту немає і все залежить від потреб продукту.

У цій секції ми ж розглянемо найпростіший варіант: єдиний об'єкт сервісу, за котрим будуть з'являтись лише деякі поди із новим кодом. Відсоток буде відраховуватись "вагою" нових подів: 1 пода із новим імеджом із 5 - це 20%, 2 поди - 40% тощо. Для більш просунутих варіантів буде потрібна інтеграція із AWS ALB, про яку поговоримо в наступній частині. На реальному прикладі це має бути більш зрозуміліше:

$ cat <<EOF | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: rollout-canary
spec:
  replicas: 5
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      app: rollout-canary
  template:
    metadata:
      labels:
        app: rollout-canary
    spec:
      containers:
      - name: rollouts-demo
        image: argoproj/rollouts-demo:blue
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
  strategy:
    canary:
      steps:
      - setWeight: 20
      - pause: {}
      - setWeight: 40
      - pause: {duration: 10}
---
apiVersion: v1
kind: Service
metadata:
  name: rollout-canary
spec:
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
  selector:
    app: rollout-canary
EOF
rollout.argoproj.io/rollout-canary created
service/rollout-canary created

На початку буде піднято 5 реплік:

$ kubectl argo rollouts get rollout rollout-canary      
Name:            rollout-canary
Namespace:       default
Status:          ✔ Healthy
Strategy:        Canary
  Step:          6/6
  SetWeight:     100
  ActualWeight:  100
Images:          argoproj/rollouts-demo:blue (stable)
Replicas:
  Desired:       5
  Current:       5
  Updated:       5
  Ready:         5
  Available:     5

NAME                                        KIND        STATUS     AGE    INFO
⟳ rollout-canary                            Rollout     ✔ Healthy  3m55s  
└──# revision:1                                                           
   └──⧉ rollout-canary-679b8b5b4c           ReplicaSet  ✔ Healthy  3m55s  stable
      ├──□ rollout-canary-679b8b5b4c-f8vzf  Pod         ✔ Running  3m55s  ready:1/1
      ├──□ rollout-canary-679b8b5b4c-mgm6k  Pod         ✔ Running  3m55s  ready:1/1
      ├──□ rollout-canary-679b8b5b4c-rk77m  Pod         ✔ Running  3m55s  ready:1/1
      ├──□ rollout-canary-679b8b5b4c-txh55  Pod         ✔ Running  3m55s  ready:1/1
      └──□ rollout-canary-679b8b5b4c-v482j  Pod         ✔ Running  3m55s  ready:1/1

$ kubectl get rs rollout-canary-679b8b5b4c -o json | jq -r ".metadata.labels"
{
  "app": "rollout-canary",
  "rollouts-pod-template-hash": "679b8b5b4c"
}


Сервіс rollout-canary буде постійно дивитись на групу по постійному селектору "app: rollout-canary" за котрим стоятимуть вже дві репліка сети і відповідно їхні поди.

Розпочнемо Canary-реліз, завдяки зміні імеджа в роллаут об'єкті:

$ kubectl argo rollouts set image rollout-canary rollouts-demo=argoproj/rollouts-demo:green

Переглянемо статус релізу:

$ kubectl argo rollouts get rollout rollout-canary

Name:            rollout-canary
Namespace:       default
Status:          ॥ Paused
Message:         CanaryPauseStep
Strategy:        Canary
  Step:          1/6
  SetWeight:     20
  ActualWeight:  20
Images:          argoproj/rollouts-demo:blue (stable)
                 argoproj/rollouts-demo:green (canary)
Replicas:
  Desired:       5
  Current:       5
  Updated:       1
  Ready:         5
  Available:     5

NAME                                        KIND        STATUS     AGE    INFO
⟳ rollout-canary                            Rollout     ॥ Paused   9m36s  
├──# revision:2                                                           
│  └──⧉ rollout-canary-5584575b9d           ReplicaSet  ✔ Healthy  24s    canary
│     └──□ rollout-canary-5584575b9d-tnmj2  Pod         ✔ Running  24s    ready:1/1
└──# revision:1                                                           
   └──⧉ rollout-canary-679b8b5b4c           ReplicaSet  ✔ Healthy  9m36s  stable
      ├──□ rollout-canary-679b8b5b4c-mgm6k  Pod         ✔ Running  9m36s  ready:1/1
      ├──□ rollout-canary-679b8b5b4c-rk77m  Pod         ✔ Running  9m36s  ready:1/1
      ├──□ rollout-canary-679b8b5b4c-txh55  Pod         ✔ Running  9m36s  ready:1/1
      └──□ rollout-canary-679b8b5b4c-v482j  Pod         ✔ Running  9m36s  ready:1/1

$ kubectl get rs rollout-canary-679b8b5b4c -o json | jq -r ".metadata.labels"
{
  "app": "rollout-canary",
  "rollouts-pod-template-hash": "679b8b5b4c"
}

$ kubectl get rs rollout-canary-5584575b9d -o json | jq -r ".metadata.labels"
{
  "app": "rollout-canary",
  "rollouts-pod-template-hash": "5584575b9d"
}

З'явилась одна нова пода в новому репліка-сеті і одна зникла зі старого. Тобто 1 нова нода із 5 це і буде 20%, як було вказано в стратегії.  

Реліз наразі зупинився і чекає ручного втручання, завдяки параметру "pause: {}". Тому треба явно відправити promote:

$ kubectl argo rollouts promote rollout-canary
rollout 'rollout-canary' promoted


Після чого відбудеться виливка коду до кінця в 2 кроки із затримкою в 10 секунд:

$ kubectl argo rollouts get rollout rollout-canary
Name:            rollout-canary
Namespace:       default
Status:          ✔ Healthy
Strategy:        Canary
  Step:          6/6
  SetWeight:     100
  ActualWeight:  100
Images:          argoproj/rollouts-demo:green (stable)
Replicas:
  Desired:       5
  Current:       5
  Updated:       5
  Ready:         5
  Available:     5

NAME                                        KIND        STATUS        AGE  INFO
⟳ rollout-canary                            Rollout     ✔ Healthy     20m  
├──# revision:2                                                            
│  └──⧉ rollout-canary-5584575b9d           ReplicaSet  ✔ Healthy     11m  stable
│     ├──□ rollout-canary-5584575b9d-tnmj2  Pod         ✔ Running     11m  ready:1/1
│     ├──□ rollout-canary-5584575b9d-5274x  Pod         ✔ Running     40s  ready:1/1
│     ├──□ rollout-canary-5584575b9d-6cfcx  Pod         ✔ Running     40s  ready:1/1
│     ├──□ rollout-canary-5584575b9d-zzgv6  Pod         ✔ Running     28s  ready:1/1
│     └──□ rollout-canary-5584575b9d-xhm9b  Pod         ✔ Running     17s  ready:1/1
└──# revision:1                                                            
   └──⧉ rollout-canary-679b8b5b4c           ReplicaSet  • ScaledDown  20m


Дії по промоуту версії також можна виконувати із веб-панелі:


2. CANARY WITH BALANCER


Наявність провайдера трафіку, що у нашому випадку виконує роль AWS LB Controller, значно розширює можливості Canary в Argo Rollouts. Останній здатен маніпулювати анотаціями Ingress-ресурсів, котрі в свою чергу можуть встановлювати точний відсоток трафіку, що піде на кожну із версій сервісу. Також є можливість підняття нової версії без автоматичного включення її в трафік, наприклад задля попереднього її тестування тощо.

Розглянемо наступний варіант:

$ cat << EOF | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: rollout-canary
spec:
  replicas: 5
  strategy:
    canary:
      canaryService: rollout-canary-canary
      stableService: rollout-canary-stable
      trafficRouting:
        alb:
          ingress: rollout-canary-stable
          servicePort: 80
      scaleDownDelaySeconds: 60
      steps:
      - setCanaryScale:
          replicas: 2
      - pause: {}
      - setWeight: 20
      - setCanaryScale:
          matchTrafficWeight: true
      - pause: {}
      - setWeight: 40
      - pause: {}
      - setWeight: 60
      - pause: {duration: 10}
      - setWeight: 80
      - pause: {duration: 10}
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: rollout-canary
  template:
    metadata:
      labels:
        app: rollout-canary
    spec:
      containers:
      - name: rollout-canary
        image: argoproj/rollouts-demo:blue
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: rollout-canary-canary
spec:
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
  selector:
    app: rollout-canary
---
apiVersion: v1
kind: Service
metadata:
  name: rollout-canary-stable
spec:
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
  selector:
    app: rollout-canary
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: rollout-canary-stable
  annotations:
    alb.ingress.kubernetes.io/target-type: ip
    # external-dns.alpha.kubernetes.io/hostname: canary.example.com
    alb.ingress.kubernetes.io/group.name: canary-group
    alb.ingress.kubernetes.io/success-codes: 200-302
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
spec:
  ingressClassName: alb
  rules:
    - host: canary.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: rollout-canary-stable
                port:
                  name: use-annotation
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: rollout-canary-canary
  annotations:
    alb.ingress.kubernetes.io/target-type: ip
    # external-dns.alpha.kubernetes.io/hostname: canary-preview.example.com
    alb.ingress.kubernetes.io/group.name: canary-group
    alb.ingress.kubernetes.io/success-codes: 200-302
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
spec:
  ingressClassName: alb
  rules:
    - host: canary-preview.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: rollout-canary-canary
                port:
                  number: 80
EOF
rollout.argoproj.io/rollout-canary created
service/rollout-canary-canary created
service/rollout-canary-stable created
ingress.networking.k8s.io/rollout-canary-stable created
ingress.networking.k8s.io/rollout-canary-canary created


Ми створили Rollout об'єкт, де вказали який сервіс буде стабільним (попередня версія коду до початку деплою), а який canary (нова версія коду, котру будуть поступово включати в загальний трафік). Як і у випадку із BlueGreen, Argo Rollouts оперуватиме додатковими селекторами сервіс-об'єктів задля того, щоб спрямувати їх до правильної групи подів (реплікасетів).

Також ми вилили два інгресс-об'єкта: один, rollout-canary-canary, буде постійно вказувати на одноіменний сервіс, а інший, rollout-canary-stable, буде мати дещо складнішу логіку. Для нього Argo Rollouts буде проставляти додатковий annotation, де буде вказано який відсоток і на який сервіс-об'єкт необхідно буде відсилати (port.name: use-annotation). Таким чином і буде досягатись відсоток трафіку, що має прямувати на нову версію коду.

Отже, початковий стан роллауту наступний:

$ kubectl argo rollouts get rollout rollout-canary 
 
Name:            rollout-canary
Namespace:       default
Status:          ✔ Healthy
Strategy:        Canary
  Step:          11/11
  SetWeight:     100
  ActualWeight:  100
Images:          argoproj/rollouts-demo:blue (stable)
Replicas:
  Desired:       5
  Current:       5
  Updated:       5
  Ready:         5
  Available:     5

NAME                                        KIND        STATUS     AGE  INFO
⟳ rollout-canary                            Rollout     ✔ Healthy  99m  
└──# revision:1                                                         
   └──⧉ rollout-canary-7487c96d7b           ReplicaSet  ✔ Healthy  95m  stable
      ├──□ rollout-canary-7487c96d7b-482gg  Pod         ✔ Running  95m  ready:1/1
      ├──□ rollout-canary-7487c96d7b-4s895  Pod         ✔ Running  95m  ready:1/1
      ├──□ rollout-canary-7487c96d7b-bb42b  Pod         ✔ Running  95m  ready:1/1
      ├──□ rollout-canary-7487c96d7b-kstlp  Pod         ✔ Running  95m  ready:1/1
      └──□ rollout-canary-7487c96d7b-vdh9v  Pod         ✔ Running  95m  ready:1/1


Зі сторони AWS справи на балансувальнику виглядають наступним чином:

2 сервіс-об'єкти завдяки TargetGroupBinding-ам (TGB), що на них посилаються, формують 3 таргет групи:

$ kubectl get targetgroupbinding
NAME   SERVICE-NAME   SERVICE-PORT   TARGET-TYPE   AGE
k8s-default-rolloutc-3563d42384   rollout-canary-canary   80             ip            116m
k8s-default-rolloutc-55889e344e   rollout-canary-canary   80             ip            119m
k8s-default-rolloutc-7d29633734   rollout-canary-stable   80             ip            115m

$ kubectl get targetgroupbinding -o wide

NAME    SERVICE-NAME   SERVICE-PORT    TARGET-TYPE   ARN   NAME   AGE
k8s-default-rolloutc-3563d42384   rollout-canary-canary   80             ip            arn:aws:elasticloadbalancing:us-east-1:789248082627:targetgroup/k8s-default-rolloutc-3563d42384/131cd3a0ef5445d9          117m
k8s-default-rolloutc-55889e344e   rollout-canary-canary   80             ip            arn:aws:elasticloadbalancing:us-east-1:789248082627:targetgroup/k8s-default-rolloutc-55889e344e/56e12919fd79b739          120m
k8s-default-rolloutc-7d29633734   rollout-canary-stable   80             ip            arn:aws:elasticloadbalancing:us-east-1:789248082627:targetgroup/k8s-default-rolloutc-7d29633734/5415dfce8a8ada28          117m


Як бачимо, rollout-canary-canary сервіс має два TGB, серед яких і буде відбуватись магія відсотків.
На початку всі таргет групи будуть вказувати на ту ж групу:

Тепер почнемо сам реліз, котрий, нагадаю, також можна почати із web-панелі:

$ kubectl argo rollouts set image rollout-canary rollout-canary=argoproj/rollouts-demo:green
rollout "rollout-canary" image updated


І перевіримо, які зміни відбулись:

$ kubectl argo rollouts get rollout rollout-canary 
Name:            rollout-canary
Namespace:       default
Status:          ॥ Paused
Message:         CanaryPauseStep
Strategy:        Canary
  Step:          1/11
  SetWeight:     0
  ActualWeight:  0
Images:          argoproj/rollouts-demo:blue (stable)
                 argoproj/rollouts-demo:green (canary)
Replicas:
  Desired:       5
  Current:       7
  Updated:       2
  Ready:         7
  Available:     7

NAME                                        KIND        STATUS     AGE   INFO
⟳ rollout-canary                            Rollout     ॥ Paused   130m  
├──# revision:2                                                          
│  └──⧉ rollout-canary-5497dcb47f           ReplicaSet  ✔ Healthy  57s   canary
│     ├──□ rollout-canary-5497dcb47f-jnfrl  Pod         ✔ Running  56s   ready:1/1
│     └──□ rollout-canary-5497dcb47f-vgsnh  Pod         ✔ Running  56s   ready:1/1
└──# revision:1                                                          
   └──⧉ rollout-canary-7487c96d7b           ReplicaSet  ✔ Healthy  126m  stable
      ├──□ rollout-canary-7487c96d7b-482gg  Pod         ✔ Running  126m  ready:1/1
      ├──□ rollout-canary-7487c96d7b-4s895  Pod         ✔ Running  126m  ready:1/1
      ├──□ rollout-canary-7487c96d7b-bb42b  Pod         ✔ Running  126m  ready:1/1
      ├──□ rollout-canary-7487c96d7b-kstlp  Pod         ✔ Running  126m  ready:1/1
      └──□ rollout-canary-7487c96d7b-vdh9v  Pod         ✔ Running  126m  ready:1/1

$ kubectl get svc                                        
NAME                    TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
rollout-canary-canary   ClusterIP   172.20.58.159    <none>        80/TCP    132m
rollout-canary-stable   ClusterIP   172.20.165.229   <none>        80/TCP    128m

$ kubectl get svc rollout-canary-canary -o json | jq -r '.spec.selector'
{
  "app": "rollout-canary",
  "rollouts-pod-template-hash": "5497dcb47f"
}

$ kubectl get svc rollout-canary-stable -o json | jq -r '.spec.selector'
{
  "app": "rollout-canary",
  "rollouts-pod-template-hash": "7487c96d7b"
}


Проте по статусу роллауту, трафік наразі йде лише на rollout-canary-stable сервіс:

$ kubectl get rollout rollout-canary -o json | jq -r ".status.canary"
{
  "weights": {
    "canary": {
      "podTemplateHash": "5497dcb47f",
      "serviceName": "rollout-canary-canary",
      "weight": 0
    },
    "stable": {
      "podTemplateHash": "7487c96d7b",
      "serviceName": "rollout-canary-stable",
      "weight": 100
    }
  }
}


Це також підтверджує сам AWS, адже відсотковий розподіл на таргет групах лишився такий самий:

Проте preview-домен canary-preview.example.com наразі буде вказувати на нову версію коду:

$ kubectl get pod rollout-canary-5497dcb47f-jnfrl -o json | jq -r '.status.podIP'
10.0.25.90

$ kubectl get pod rollout-canary-5497dcb47f-vgsnh -o json | jq -r '.status.podIP'
10.0.18.113

Це робиться для можливості перевірити роботу нового коду перед фактичним його введенням в трафік. Звісно, якщо це необхідно.

Таргет група, на яку йде і досі 100% трафіку залишилась без змін:

Інша ж отримала ті ж поди що і перша, проте на неї основні запити ще не йдуть: 
Дві останні таргет групи обслуговують єдиний домен canary.example.com, котрий і є основним. Ситуація із розподілом трафіку також відображена на rollout-canary-stable інгресі і це вже справа рук самого Argo Rollouts котролера:

$ kubectl get ing
NAME   CLASS   HOSTS    ADDRESS    PORTS   AGE
rollout-canary-canary   alb     canary-preview.example.com   internal-k8s-canarygroup-17ccba5dee-147664472.us-east-1.elb.amazonaws.com   80      173m
rollout-canary-stable   alb     canary.example.com           internal-k8s-canarygroup-17ccba5dee-147664472.us-east-1.elb.amazonaws.com   80      173m

$ kubectl get ing rollout-canary-stable -o json | jq -r '.metadata.annotations'

{
  "alb.ingress.kubernetes.io/actions.rollout-canary-stable": "{\"Type\":\"forward\",\"ForwardConfig\":{\"TargetGroups\":[{\"ServiceName\":\"rollout-canary-canary\",\"ServicePort\":\"80\",\"Weight\":0},{\"ServiceName\":\"rollout-canary-stable\",\"ServicePort\":\"80\",\"Weight\":100}]}}",
  ...
}


Анотація вказує на те, що 100% трафіку отримують саме поди під rollout-canary-stable сервісом, незалежно від того, що поди із новою версією були підняті також. Це можливо завдяки setCanaryScale опції роллаут-стратегії.

Наразі роллаут потребує ручного промоута наступного кроку завдяки параметру стратегії "pause: {}":

$ kubectl argo rollouts promote rollout-canary
rollout 'rollout-canary' promoted


Подивимось які зміни відбулись:

$ kubectl argo rollouts get rollout rollout-canary 
Name:            rollout-canary
Namespace:       default
Status:          ॥ Paused
Message:         CanaryPauseStep
Strategy:        Canary
  Step:          4/11
  SetWeight:     20
  ActualWeight:  20
Images:          argoproj/rollouts-demo:blue (stable)
                 argoproj/rollouts-demo:green (canary)
Replicas:
  Desired:       5
  Current:       6
  Updated:       1
  Ready:         6
  Available:     6

NAME                                        KIND        STATUS     AGE   INFO
⟳ rollout-canary                            Rollout     ॥ Paused   3h4m  
├──# revision:2                                                          
│  └──⧉ rollout-canary-5497dcb47f           ReplicaSet  ✔ Healthy  55m   canary
│     └──□ rollout-canary-5497dcb47f-jnfrl  Pod         ✔ Running  55m   ready:1/1
└──# revision:1                                                          
   └──⧉ rollout-canary-7487c96d7b           ReplicaSet  ✔ Healthy  3h1m  stable
      ├──□ rollout-canary-7487c96d7b-482gg  Pod         ✔ Running  3h1m  ready:1/1
      ├──□ rollout-canary-7487c96d7b-4s895  Pod         ✔ Running  3h1m  ready:1/1
      ├──□ rollout-canary-7487c96d7b-bb42b  Pod         ✔ Running  3h1m  ready:1/1
      ├──□ rollout-canary-7487c96d7b-kstlp  Pod         ✔ Running  3h1m  ready:1/1
      └──□ rollout-canary-7487c96d7b-vdh9v  Pod         ✔ Running  3h1m  ready:1/1

Завдяки кроку "setWeight: 20" було проставлено 20% трафіку на нову версію, а "setCanaryScale.matchTrafficWeight: true" повернув відповідність відсотку трафіку до відсотку под, що його обслуговують. Ця логіка активована по-замовчуванню, і її необхідно явно активувати після користування опцією ручного управління репліками setCanaryScale. Саме тому на новій версії залишилась лише 1 пода, котра кількісно і є 20-ма відсотками потужностей.

Балансувальник та таргет групи також все це відображають:
$ kubectl get ing rollout-canary-stable -o json | jq -r '.metadata.annotations'
{
  "alb.ingress.kubernetes.io/actions.rollout-canary-stable": "{\"Type\":\"forward\",\"ForwardConfig\":{\"TargetGroups\":[{\"ServiceName\":\"rollout-canary-canary\",\"ServicePort\":\"80\",\"Weight\":20},{\"ServiceName\":\"rollout-canary-stable\",\"ServicePort\":\"80\",\"Weight\":80}]}}",
  ...
}


Тож під основним доменом canary.example.com вже 20% трафіку піде на поди із новим кодом. Preview-домен canary-preview.example.com вже особливо не цікавить, бо він виконав свою роль:

Наступний крок збільшить відсоток трафіку нової версії до 40:

$ kubectl argo rollouts promote rollout-canary


$ kubectl get ing rollout-canary-stable -o json | jq -r '.metadata.annotations'
{
  "alb.ingress.kubernetes.io/actions.rollout-canary-stable": "{\"Type\":\"forward\",\"ForwardConfig\":{\"TargetGroups\":[{\"ServiceName\":\"rollout-canary-canary\",\"ServicePort\":\"80\",\"Weight\":40},{\"ServiceName\":\"rollout-canary-stable\",\"ServicePort\":\"80\",\"Weight\":60}]}}",
...
}

$ kubectl argo rollouts get rollout rollout-canary 
Name:            rollout-canary
Namespace:       default
Status:          ॥ Paused
Message:         CanaryPauseStep
Strategy:        Canary
  Step:          6/11
  SetWeight:     40
  ActualWeight:  40
Images:          argoproj/rollouts-demo:blue (stable)
                 argoproj/rollouts-demo:green (canary)
Replicas:
  Desired:       5
  Current:       7
  Updated:       2
  Ready:         7
  Available:     7

NAME                                        KIND        STATUS     AGE    INFO
⟳ rollout-canary                            Rollout     ॥ Paused   3h28m  
├──# revision:2                                                           
│  └──⧉ rollout-canary-5497dcb47f           ReplicaSet  ✔ Healthy  79m    canary
│     ├──□ rollout-canary-5497dcb47f-jnfrl  Pod         ✔ Running  79m    ready:1/1
│     └──□ rollout-canary-5497dcb47f-btdjf  Pod         ✔ Running  62s    ready:1/1
└──# revision:1                                                           
   └──⧉ rollout-canary-7487c96d7b           ReplicaSet  ✔ Healthy  3h25m  stable
      ├──□ rollout-canary-7487c96d7b-482gg  Pod         ✔ Running  3h25m  ready:1/1
      ├──□ rollout-canary-7487c96d7b-4s895  Pod         ✔ Running  3h25m  ready:1/1
      ├──□ rollout-canary-7487c96d7b-bb42b  Pod         ✔ Running  3h25m  ready:1/1
      ├──□ rollout-canary-7487c96d7b-kstlp  Pod         ✔ Running  3h25m  ready:1/1
      └──□ rollout-canary-7487c96d7b-vdh9v  Pod         ✔ Running  3h25m  ready:1/1


Після чого відсоток трафіку із двома паузами в 10 секунд буде доведено до 100%:

$ kubectl argo rollouts promote rollout-canary

$ kubectl get ing rollout-canary-stable -o json | jq -r '.metadata.annotations'
{
  "alb.ingress.kubernetes.io/actions.rollout-canary-stable": "{\"Type\":\"forward\",\"ForwardConfig\":{\"TargetGroups\":[{\"ServiceName\":\"rollout-canary-canary\",\"ServicePort\":\"80\",\"Weight\":0},{\"ServiceName\":\"rollout-canary-stable\",\"ServicePort\":\"80\",\"Weight\":100}]}}",
  ...
}

$ kubectl argo rollouts get rollout rollout-canary                     Name:            rollout-canary
Namespace:       default
Status:          ✔ Healthy
Strategy:        Canary
  Step:          11/11
  SetWeight:     100
  ActualWeight:  100
Images:          argoproj/rollouts-demo:green (stable)
Replicas:
  Desired:       5
  Current:       5
  Updated:       5
  Ready:         5
  Available:     5

NAME                                        KIND        STATUS        AGE    INFO
⟳ rollout-canary                            Rollout     ✔ Healthy     3h34m  
├──# revision:2                                                              
│  └──⧉ rollout-canary-5497dcb47f           ReplicaSet  ✔ Healthy     85m    stable
│     ├──□ rollout-canary-5497dcb47f-jnfrl  Pod         ✔ Running     85m    ready:1/1
│     ├──□ rollout-canary-5497dcb47f-btdjf  Pod         ✔ Running     7m15s  ready:1/1
│     ├──□ rollout-canary-5497dcb47f-9jhct  Pod         ✔ Running     3m37s  ready:1/1
│     ├──□ rollout-canary-5497dcb47f-wf5zl  Pod         ✔ Running     3m14s  ready:1/1
│     └──□ rollout-canary-5497dcb47f-xwjq7  Pod         ✔ Running     3m2s   ready:1/1
└──# revision:1                                                              
   └──⧉ rollout-canary-7487c96d7b           ReplicaSet  • ScaledDown  3h31m


На сервіс rollout-canary-stable буде знову прямувати 100% трафіку, проте за ним (як і за preview звісно) вже стоятимуть поди із новою версією коду:

$ kubectl get svc rollout-canary-stable -o json | jq -r '.spec.selector'
{
  "app": "rollout-canary",
  "rollouts-pod-template-hash": "5497dcb47f"
}

$ kubectl get svc rollout-canary-canary -o json | jq -r '.spec.selector'

{
  "app": "rollout-canary",
  "rollouts-pod-template-hash": "5497dcb47f"
}


Тобто на останньому етапі Argo Rollouts переписав селектор для rollout-canary-stable на поди із новою версією коду 5497dcb47f, хоча раніше він був на 7487c96d7b.

Логіка хоч і не проста, але доволі зрозуміла. У кінці релізу, як і до його початку, сервіси rollout-canary-stable та rollout-canary-canary будуть вказувати на ті ж поди, а із початком релізу сервіс rollout-canary-canary буде "збирати" під собою поди нової версії. Недоліком цієї схеми є те, що із деякими контролерами (наприклад AWS LB Controller) управління трафіком на останньому етапі зміни селекторів можуть з'являтись 500-ті помилки для клієнтів. Причиною цього є те, що поява подів в кожній групи управляється за допомогою Pod Readiness Gate (так, ліпше його активувати). Але він жодним чином не регулює зміну адрес подів в кожній AWS таргет групі на етапі зміни селекторів в сервіс-об'єктах.

Ця ж особливість присутня і у імплементації BlueGreen, про котру я писав у попередній статті.

Знову ж, це може не бути проблемою, адже клієнти повинні володіти retry-логікою у разі появи подібних помилок. Якщо ж це все таки критично - то ліпше користуватись Canary PingPong стратегією, де не відбувається зміни селекторів на останньому етапі.


3. CANARY PING-PONG STRATEGY

Ця стратегія можлива лише із залученням контролерів управління трафіком і має інші параметри ніж попередня, хоч дещо схожу логіку:

$ cat << EOF | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: rollout-pingpong
spec:
  replicas: 5
  strategy:
    canary:
      pingPong:
        pingService: rollout-ping
        pongService: rollout-pong
      trafficRouting:
        alb:
          ingress: rollout-pingpong
          rootService: rollout-ping
          servicePort: 80
      scaleDownDelaySeconds: 60
      steps:
      - setWeight: 10
      - pause: {}
      - setWeight: 60
      - pause: {duration: 10}
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: rollout-pingpong
  template:
    metadata:
      labels:
        app: rollout-pingpong
    spec:
      containers:
      - name: rollout-pingpong
        image: argoproj/rollouts-demo:blue
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: rollout-ping
spec:
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
  selector:
    app: rollout-pingpong
---
apiVersion: v1
kind: Service
metadata:
  name: rollout-pong
spec:
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
  selector:
    app: rollout-pingpong
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: rollout-pingpong
  annotations:
    alb.ingress.kubernetes.io/target-type: ip
    # external-dns.alpha.kubernetes.io/hostname: pingpong.example.com
    alb.ingress.kubernetes.io/group.name: pingpong-group
    alb.ingress.kubernetes.io/success-codes: 200-302
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
spec:
  ingressClassName: alb
  rules:
    - host: pingpong.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: rollout-ping
                port:
                  name: use-annotation
EOF
rollout.argoproj.io/rollout-pingpong created
service/rollout-ping created
service/rollout-pong created
ingress.networking.k8s.io/rollout-pingpong created


До початку релізу на стороні AWS маємо одне правило в listener на 2 таргет групи, котрі із самого початку вказують на ті ж поди:

Традиційно відсотком в правилі керує AWS LB Controller через анотацію в Ingress:

$ kubectl get ing                                                      
NAME    CLASS   HOSTS   ADDRESS PORTS   AGE
rollout-pingpong   alb     pingpong.example.com   internal-k8s-pingponggroup-7eed16b808-473890389.us-east-1.elb.amazonaws.com   80      17m


$ kubectl get ing rollout-pingpong -o json | jq -r '.metadata.annotations'
{
  "alb.ingress.kubernetes.io/actions.rollout-ping": "{\"Type\":\"forward\",\"ForwardConfig\":{\"TargetGroups\":[{\"ServiceName\":\"rollout-pong\",\"ServicePort\":\"80\",\"Weight\":0},{\"ServiceName\":\"rollout-ping\",\"ServicePort\":\"80\",\"Weight\":100}]}}"
  ...
}


Тобто наразі 100% трафіку прямує на сервіс rollout-ping, а rollout-pong, котрий буде майбутнім Canary (тобто новою версією коду), наразі не отримує трафіку.

$ kubectl argo rollouts get rollout rollout-pingpong
Name:            rollout-pingpong
Namespace:       default
Status:          ✔ Healthy
Strategy:        Canary
  Step:          4/4
  SetWeight:     100
  ActualWeight:  100
Images:          argoproj/rollouts-demo:blue (stable, ping)
Replicas:
  Desired:       5
  Current:       5
  Updated:       5
  Ready:         5
  Available:     5

NAME                                          KIND        STATUS     AGE  INFO
⟳ rollout-pingpong                            Rollout     ✔ Healthy  21m  
└──# revision:1                                                           
   └──⧉ rollout-pingpong-86468fd556           ReplicaSet  ✔ Healthy  21m  stable,ping
      ├──□ rollout-pingpong-86468fd556-2s7tl  Pod         ✔ Running  21m  ready:1/1
      ├──□ rollout-pingpong-86468fd556-97x92  Pod         ✔ Running  21m  ready:1/1
      ├──□ rollout-pingpong-86468fd556-ltwfl  Pod         ✔ Running  21m  ready:1/1
      ├──□ rollout-pingpong-86468fd556-rthjw  Pod         ✔ Running  21m  ready:1/1
      └──□ rollout-pingpong-86468fd556-x6kn2  Pod         ✔ Running  21m  ready:1/1


Ну і селектори на сервісах однакові, що доводить вже попередню ситуацію за таргет групами:

$ kubectl get svc rollout-ping -o json | jq -r '.spec.selector'        
{
  "app": "rollout-pingpong",
  "rollouts-pod-template-hash": "86468fd556"
}

$ kubectl get svc rollout-pong -o json | jq -r '.spec.selector'
{
  "app": "rollout-pingpong"
}


Почнемо реліз, після чого подивимось, що станеться із об'єктами AWS та Kubernetes:

$ kubectl argo rollouts set image rollout-pingpong rollout-pingpong=argoproj/rollouts-demo:green

$ kubectl argo rollouts get rollout rollout-pingpong
Name:            rollout-pingpong
Namespace:       default
Status:          ॥ Paused
Message:         CanaryPauseStep
Strategy:        Canary
  Step:          1/4
  SetWeight:     10
  ActualWeight:  10
Images:          argoproj/rollouts-demo:blue (stable, ping)
                 argoproj/rollouts-demo:green (canary, pong)
Replicas:
  Desired:       5
  Current:       6
  Updated:       1
  Ready:         6
  Available:     6

NAME                                          KIND        STATUS     AGE  INFO
⟳ rollout-pingpong                            Rollout     ॥ Paused   26m  
├──# revision:2                                                           
│  └──⧉ rollout-pingpong-6b5d458dd7           ReplicaSet  ✔ Healthy  16s  canary,pong
│     └──□ rollout-pingpong-6b5d458dd7-khbsw  Pod         ✔ Running  15s  ready:1/1
└──# revision:1                                                           
   └──⧉ rollout-pingpong-86468fd556           ReplicaSet  ✔ Healthy  26m  stable,ping
      ├──□ rollout-pingpong-86468fd556-2s7tl  Pod         ✔ Running  26m  ready:1/1
      ├──□ rollout-pingpong-86468fd556-97x92  Pod         ✔ Running  26m  ready:1/1
      ├──□ rollout-pingpong-86468fd556-ltwfl  Pod         ✔ Running  26m  ready:1/1
      ├──□ rollout-pingpong-86468fd556-rthjw  Pod         ✔ Running  26m  ready:1/1
      └──□ rollout-pingpong-86468fd556-x6kn2  Pod         ✔ Running  26m  ready:1/1

$ kubectl get ing rollout-pingpong -o json | jq -r '.metadata.annotations'
{
  "alb.ingress.kubernetes.io/actions.rollout-ping": "{\"Type\":\"forward\",\"ForwardConfig\":{\"TargetGroups\":[{\"ServiceName\":\"rollout-pong\",\"ServicePort\":\"80\",\"Weight\":10},{\"ServiceName\":\"rollout-ping\",\"ServicePort\":\"80\",\"Weight\":90}]}}",
...
}

$ kubectl get svc rollout-ping -o json | jq -r '.spec.selector'        
{
  "app": "rollout-pingpong",
  "rollouts-pod-template-hash": "86468fd556"
}

$ kubectl get svc rollout-pong -o json | jq -r '.spec.selector'        
{
  "app": "rollout-pingpong",
  "rollouts-pod-template-hash": "6b5d458dd7"
}


Цього разу 10% трафіку прямує на нову версію rollout-pong, а 90% на rollout-ping (стабільна версія).

Наступний промоут збільшить відсоток трафіку, що попрямує на нову версію коду, до 60% і після 10 секунд очікування автоматично доведе трафік до 100%:

$ kubectl argo rollouts promote rollout-pingpong
rollout 'rollout-pingpong' promoted

$ kubectl argo rollouts get rollout rollout-pingpong
Name:            rollout-pingpong
Namespace:       default
Status:          ✔ Healthy
Strategy:        Canary
  Step:          4/4
  SetWeight:     100
  ActualWeight:  100
Images:          argoproj/rollouts-demo:green (stable, pong)
Replicas:
  Desired:       5
  Current:       5
  Updated:       5
  Ready:         5
  Available:     5

NAME                                          KIND        STATUS        AGE    INFO
⟳ rollout-pingpong                            Rollout     ✔ Healthy     39m    
├──# revision:2                                                                
│  └──⧉ rollout-pingpong-6b5d458dd7           ReplicaSet  ✔ Healthy     13m    stable,pong
│     ├──□ rollout-pingpong-6b5d458dd7-khbsw  Pod         ✔ Running     13m    ready:1/1
│     ├──□ rollout-pingpong-6b5d458dd7-6455q  Pod         ✔ Running     2m39s  ready:1/1
│     ├──□ rollout-pingpong-6b5d458dd7-bjk8w  Pod         ✔ Running     2m39s  ready:1/1
│     ├──□ rollout-pingpong-6b5d458dd7-h2p6x  Pod         ✔ Running     2m17s  ready:1/1
│     └──□ rollout-pingpong-6b5d458dd7-q65zw  Pod         ✔ Running     2m17s  ready:1/1
└──# revision:1                                                                
   └──⧉ rollout-pingpong-86468fd556           ReplicaSet  • ScaledDown  39

$ kubectl get ing rollout-pingpong -o json | jq -r '.metadata.annotations'
{
  "alb.ingress.kubernetes.io/actions.rollout-ping": "{\"Type\":\"forward\",\"ForwardConfig\":{\"TargetGroups\":[{\"ServiceName\":\"rollout-ping\",\"ServicePort\":\"80\",\"Weight\":0},{\"ServiceName\":\"rollout-pong\",\"ServicePort\":\"80\",\"Weight\":100}]}}",
...
}


Тут цікавий момент, що після завершення релізу 100% запитів так і потрапляють на сервіс rollout-pong без змін селекторів на сервісах:

$ kubectl get svc rollout-ping -o json | jq -r '.spec.selector'   
{
  "app": "rollout-pingpong",
  "rollouts-pod-template-hash": "86468fd556"
}

$ kubectl get svc rollout-pong -o json | jq -r '.spec.selector' 
{
  "app": "rollout-pingpong",
  "rollouts-pod-template-hash": "6b5d458dd7"
}

$ kubectl get pods                                                    
NAME                                READY   STATUS    RESTARTS   AGE
rollout-pingpong-6b5d458dd7-6455q   1/1     Running   0          6m14s
rollout-pingpong-6b5d458dd7-bjk8w   1/1     Running   0          6m14s
rollout-pingpong-6b5d458dd7-h2p6x   1/1     Running   0          5m52s
rollout-pingpong-6b5d458dd7-khbsw   1/1     Running   0          17m
rollout-pingpong-6b5d458dd7-q65zw   1/1     Running   0          5m52s

$ kubectl get targetgroupbinding -o wide
NAME   SERVICE-NAME   SERVICE-PORT   TARGET-TYPE   ARN NAME   AGE
k8s-default-rolloutp-1ca7b121b6   rollout-ping   80             ip            arn:aws:elasticloadbalancing:us-east-1:789248082627:targetgroup/k8s-default-rolloutp-1ca7b121b6/820015d99b453aa0          44m
k8s-default-rolloutp-7ddac9b339   rollout-pong   80             ip            arn:aws:elasticloadbalancing:us-east-1:789248082627:targetgroup/k8s-default-rolloutp-7ddac9b339/c2213b9429d2690a          44m
Тобто останній етап зміни селекторів на стабільний хеш відстуній, а отже це унеможливлює 500-ті помилки в цей час.

Після завершення наступного реліза стабільною версію вже буде rollout-ping і так далі по колу. Тож саме тому відсутня можливість прив'язки додаткового інгреса на canary версію, адже її сервіс не має фіксованого імені. Знову ж, навряд це потрібно всім.

Детальніше про ці проблеми тут:
https://github.com/argoproj/argo-rollouts/issues/1283
https://github.com/argoproj/argo-rollouts/issues/1453
https://github.com/kubernetes-sigs/aws-load-balancer-controller/issues/2061
https://docs.google.com/presentation/d/1JnvlE-oKL7HPErwFnBBhH2pfWUf0kSoFRLUDt2Glc6E

Немає коментарів:

Дописати коментар