Kubernetes: типы Deployment Strategies и Argo Rollouts

Автор: | 21/05/2021

Одна из целей, которые мы преследуем внедряя ArgoCD в Kubernetes – использование новых Deployment Strategies для наших приложений.

Ниже рассмотрим типы деплойментов в Kubernetes, как работают Deployment в Kubernetes, и быстрый пример использования Argo Rollouts, который более детально будем рассматривать в следущих постах.

Deployment Strategies и Kubernetes

Кратко рассмотрим стратегии деплоя, имеющиеся в Kubernetes.

Сам Kubernetes “из коробки” предоставляет два типа .spec.strategy.typeRecreate и RollingUpdate, который явлется типом по-умолчанию.

Также, Kubernetes позволяет реализовать аналоги Canary и Blue-Green deployments, хотя с ограничениями.

См. документацию тут>>>.

Recreate

Тут всё достаточно просто: при этой стратегии, Kubernetes останавливает все запущенные в Deployment поды, и на их месте запускает новые.

Очевидно, что при таком подходе будет определённый даунтайм : пока остановятся старые поды (см. Pod Lifecycle — Termination of Pods), пока запустятся новые, пока они пройдут все Readiness проверки – приложение будет недоступно для пользователей.

Имеет смысл использовать его, если ваше приложение не может функционировать с двумя разными версиями одновременно, например из-за ограничений работы с базой данных.

Пример такого деплоймента:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-deploy
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello-pod
      version: "1.0"
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: hello-pod
    spec:
      containers:
      - name: hello-pod
        image: nginxdemos/hello
        ports:
        - containerPort: 80

Деплоим version: "1.0":

[simterm]

$ kubectl apply -f deployment.yaml 
deployment.apps/hello-deploy created

[/simterm]

Проверяем поды:

[simterm]

$ kubectl get pod -l app=hello-pod
NAME                            READY   STATUS    RESTARTS   AGE
hello-deploy-77bcf495b7-b2s2x   1/1     Running   0          9s
hello-deploy-77bcf495b7-rb8cb   1/1     Running   0          9s

[/simterm]

Обновляем label на version: "2.0", передеплоиваем, проверяем снова:

[simterm]

$ kubectl get pod -l app=hello-pod
NAME                           READY   STATUS        RESTARTS   AGE
hello-deploy-dd584d88d-vv5bb   0/1     Terminating   0          51s
hello-deploy-dd584d88d-ws2xp   0/1     Terminating   0          51s

[/simterm]

Оба пода убиваются, и затем создаются новые:

[simterm]

$ kubectl get pod -l app=hello-pod
NAME                           READY   STATUS    RESTARTS   AGE
hello-deploy-d6c989569-c67vt   1/1     Running   0          27s
hello-deploy-d6c989569-n7ktz   1/1     Running   0          27s

[/simterm]

Rolling Update

С RollingUpdate всё немного интереснее: тут Kubernetes запускает новые поды параллельно с запущенными старыми, а затем убивает старые, и оставляет только новые. Таким образом, в процессе деплоя некоторое время одновременно работают две версии приложения – и старое, и новое. Является типом по-умолчанию.

При таком подходе получаем zero downtime, так как в процессе обновления часть подов со старой версией остаётся жива.

Из недостатков – могут быть ситуации, когда такой подход неприменим. Например, если при старте подов выполняются MySQL-миграции, которые меняют схему базы данных таким образом, что предыдущая версия приложения не сможет её использовать.

Пример такого деплоймента:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-deploy
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello-pod
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  template:
    metadata:
      labels:
        app: hello-pod
        version: "1.0"
    spec:
      containers:
      - name: hello-pod
        image: nginxdemos/hello
        ports:
        - containerPort: 80

Тут мы изменили strategy.type: Recreate на type: RollingUpdate, и добавили два опциональных поля, которые определяют поведение Деплоймента во время выполнения обновления:

  • maxUnavailable: как много подов из replicas могут быть уничтожены для запуска новых. Может быть задано явно в количестве или в процентах.
  • maxSurge: как много подов может быть создано сверх значения, заданного в replicas. Может быть задано явно в количестве или в процентах.

В примере выше мы задали 0 в maxUnavailable, т.е. не останаливать запущенные поды, пока не будут созданы новые, и maxSurge указали в 1, т.е. во время обновления должен быть создан один дополнительный новый под, и только после того, как он перейдёт в статус Running – будет остановлен один из старых подов.

Деплоим с версией 1.0:

[simterm]

$ kubectl apply -f deployment.yaml 
deployment.apps/hello-deploy created

[/simterm]

Получаем два пода:

[simterm]

$ kubectl get pod -l app=hello-pod
NAME                           READY   STATUS              RESTARTS   AGE
hello-deploy-dd584d88d-84qk4   0/1     ContainerCreating   0          3s
hello-deploy-dd584d88d-cgc5v   1/1     Running             0          3s

[/simterm]

Обновляем версию на 2.0, деплоим ещё раз, и проверяем:

[simterm]

$ kubectl get pod -l app=hello-pod
NAME                           READY   STATUS              RESTARTS   AGE
hello-deploy-d6c989569-dkz7d   0/1     ContainerCreating   0          3s
hello-deploy-dd584d88d-84qk4   1/1     Running             0          55s
hello-deploy-dd584d88d-cgc5v   1/1     Running             0          55s

[/simterm]

Получили один под сверх заданного replicas на время обновления.

Kubernetes Canary Deployment

Тип Canary подразумевает запуск новых подов одновременно с запущенными старыми, по аналогии с RollingUpdate, но даёт больше контроля над процессом обновления.

После запуска новой версии приложения – часть запросов переключается на неё, а часть продолжает использовать старую версию.

Если с новой версией всё хорошо – то оставшиеся пользователи тоже переключаются на новую версию, а старая удаляется.

Canary тип не включён в .spec.strategy.type, но его можно реализовать без дополнительных контроллеров механизмами самого Kubernetes.

При этом, решение получается достаточно примитивным и сложным в реализации и управлении.

Для того, что бы реализовать Canary потребуется два Deployment, в которых мы зададим разные версии приложения, но в Service, который направляет трафик к подам, используем один и тот же набор labels в его selector.

Создаём два Deployment и Service с типом LoadBalancer.

В Deployment-1 указываем replicas=2, а для Deployment-2 – 0:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-deploy-1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello-pod
  template:
    metadata:
      labels:
        app: hello-pod
        version: "1.0"
    spec:
      containers:
      - name: hello-pod
        image: nginxdemos/hello
        ports:
        - containerPort: 80
        lifecycle:
          postStart:
            exec:
              command: ["/bin/sh", "-c", "echo 1 > /usr/share/nginx/html/index.html"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-deploy-2
spec:
  replicas: 0
  selector:
    matchLabels:
      app: hello-pod
  template:
    metadata:
      labels:
        app: hello-pod
        version: "2.0"
    spec:
      containers:
      - name: hello-pod
        image: nginxdemos/hello
        ports:
        - containerPort: 80
        lifecycle:
          postStart:
            exec:
              command: ["/bin/sh", "-c", "echo 2 > /usr/share/nginx/html/index.html"]
---
apiVersion: v1
kind: Service 
metadata:
  name: hello-svc
spec:
  type: LoadBalancer
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
  selector:
    app: hello-pod

С помощью postStart перезапишем значение индексного файла, что бы потом иметь возможность увидеть, к какому именно поду мы обращаемся.

Деплоим:

[simterm]

$ kubectl apply -f deployment.yaml 
deployment.apps/hello-deploy-1 created
deployment.apps/hello-deploy-2 created
service/hello-svc created

[/simterm]

Проверяем поды:

[simterm]

$ kubectl get pod -l app=hello-pod
NAME                             READY   STATUS    RESTARTS   AGE
hello-deploy-1-dd584d88d-25rbx   1/1     Running   0          71s
hello-deploy-1-dd584d88d-9xsng   1/1     Running   0          71s

[/simterm]

Сейчас Service отправляет весь трафик на поды из первого деплоймента:

[simterm]

$ curl adb469658008c41cd92a93a7adddd235-1170089858.us-east-2.elb.amazonaws.com
1

$ curl adb469658008c41cd92a93a7adddd235-1170089858.us-east-2.elb.amazonaws.com
1

[/simterm]

Теперь можем обновить Deployment-2, задав ему replicas: 1:

[simterm]

$ kubectl patch deployment.v1.apps/hello-deploy-2 -p '{"spec":{"replicas": 1}}'
deployment.apps/hello-deploy-2 patched

[/simterm]

Получаем три пода с одинаковой label app=hello-pod:

[simterm]

$ kubectl get pod -l app=hello-pod
NAME                             READY   STATUS    RESTARTS   AGE
hello-deploy-1-dd584d88d-25rbx   1/1     Running   0          3m2s
hello-deploy-1-dd584d88d-9xsng   1/1     Running   0          3m2s
hello-deploy-2-d6c989569-x2lsb   1/1     Running   0          6s

[/simterm]

Соответственно, Service будет отправлять 70% запросов на поды из первого деплоймента, а 30 – на поды второго деплоймента:

[simterm]

$ curl adb***858.us-east-2.elb.amazonaws.com
1
$ curl adb***858.us-east-2.elb.amazonaws.com
1
$ curl adb***858.us-east-2.elb.amazonaws.com
1
$ curl adb***858.us-east-2.elb.amazonaws.com
1
$ curl adb***858.us-east-2.elb.amazonaws.com
1
$ curl adb***858.us-east-2.elb.amazonaws.com
1
$ curl adb***858.us-east-2.elb.amazonaws.com
2
$ curl adb***858.us-east-2.elb.amazonaws.com
1
$ curl adb***858.us-east-2.elb.amazonaws.com
2

[/simterm]

После проверки version 2.0 – можно удалить старый деплоймент, а новый заскейлить до 2 подов.

Kubernetes Blue/Green Deployment

Используя этот же механизм – можем реализовать и аналог blue-green деплоймента, когда у нас одновременно работает и старая (green), и новая (blue) версия, но весь трафик направляет на новую, а если с ней возникают проблемы – то можно переключиться обратно на первую.

Для этого в .spec.selector Сервиса добавим выборку подов первого, “green”, деплоймента , используя label version:

...
  selector:
    app: hello-pod
    version: "1.0"

Передеплоиваем, проверяем:

[simterm]

14:34:18 [setevoy@setevoy-arch-work ~/Temp]  $ curl adb***858.us-east-2.elb.amazonaws.com
1
14:34:18 [setevoy@setevoy-arch-work ~/Temp]  $ curl adb***858.us-east-2.elb.amazonaws.com
1

[/simterm]

Меняем selector на version: 2 – переключаем трафик на blue-версию:

[simterm]

$ kubectl patch services/hello-svc -p '{"spec":{"selector":{"version": "2.0"}}}'
service/hello-svc patched

[simterm]

Проверяем:

[simterm]

14:37:32 [setevoy@setevoy-arch-work ~/Temp]  $ curl adb***858.us-east-2.elb.amazonaws.com
2
14:37:33 [setevoy@setevoy-arch-work ~/Temp]  $ curl adb***858.us-east-2.elb.amazonaws.com
2

[/simterm]

После того, как всё заработало – удаляется старая версия, а blue-приложение становится green.

При использовании и Canary, и Blue-Green по схемам, описанным выше, мы получаем целую пачку проблем – и необходимость самим менеджить разные Deployments, и отслеживать статус новых версий, анализировать трафик к новым сервисам на предмет ошибок, и т.д.

Вместо этого – их можно реализовать с помощью Istio или ArgoCD.

Istio потрогаем попозже, а вот ArgoCD кратенько рассмотрим сейчас.

Deployment и ReplicaSet

Пред тем, как продолжать – разберёмся, как вообще работает Deployment и процесс выполнения апдейтов.

Итак, Deployment – это объект Kubernetes, в котором мы описываем шаблон для создания подов и их количество.

После создания Деплоймента – он в свою очередь создаёт объект ReplicaSet, который управляет непосредственно подами – их состоянием и количеством.

Во время обновления Деплоймента – он создаёт новый ReplicaSet с новой конфигурацией, а ReplicaSet в свою очередь, создаёт новые поды:

См. Updating a Deployment.

Каждый под, созданный с помощью Deployment, имеет связанный с этим подом ReplicaSet, в ReplicaSet в свою очередь – имеет указатель на Deployment, который его “породил”.

Проверим под:

[simterm]

$ kubectl describe pod hello-deploy-d6c989569-96gqc
Name:               hello-deploy-d6c989569-96gqc
...
Labels:             app=hello-pod
...
Controlled By:      ReplicaSet/hello-deploy-d6c989569
...

[/simterm]

Этот под Controlled By: ReplicaSet/hello-deploy-d6c989569, проверяем этот ReplicaSet:

[simterm]

$ kubectl describe replicaset hello-deploy-d6c989569
...
Controlled By:  Deployment/hello-deploy
...

[/simterm]

Вот и наш Деплоймент – Controlled By: Deployment/hello-deploy.

Ну и шаблон ReplicaSet по сути является копией полей spec.template этого Deployment:

[simterm]

$ kubectl describe replicaset hello-deploy-2-8878b4b
...
Pod Template:
  Labels:  app=hello-pod
           pod-template-hash=8878b4b
           version=2.0
  Containers:
   hello-pod:
    Image:        nginxdemos/hello
    Port:         80/TCP
    Host Port:    0/TCP
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Events:           <none>

[/simterm]

Теперь посмотрим, как работает Argo Rollouts.

Argo Rollouts

Документация – https://argoproj.github.io/argo-rollouts.

Argo Rollouts представляет собой Kubernetes контроллер и набор Kubernetes Custom Resource Definitions, которые вместе позволяют выполнение более сложных деплоев в Kubernetes.

Может использоваться сам по себе, интегрироваться с Ingress контроллерами, такими как ALB или NGINX, или различными Service Mesh типа Istio.

Во время деплоя, Argo Rollouts может сам выполнить анализ новых версий приложения и выполнить rollback в случае проблем.

Для использвания Rollouts вместо ресурса Deployment мы создаём новые ресурсы – Rollout, в spec.strategy которого и описываем желаемый тип деплоя и его параметры, например:

...
spec:
  replicas: 5
  strategy:
    canary:
      steps:
      - setWeight: 20
...

Остальные же поля аналогичны стандартному Kubernetes Deployment.

Как и Deployment – Rollout использует ReplicaSet для запуска новых подов.

При этом, после установки Argo Rollouts можно пользоваться как привычнми Deployment с их типами spec.strategy, так и новым ресурсом Rollout. Также, можно достаточно легко мигрировать существующие Deployment в Rollout, см. Convert Deployment to Rollout.

См. Architecture, Rollout Specification и Updating a Rollout.

Установка Argo Rollouts

Создаём отдельный Namespace:

[simterm]

$ kubectl create namespace argo-rollouts
namespace/argo-rollouts created

[/simterm]

Деплоим манифест, из которого будут созданы необходимые Kubernetes CRD, ServiceAccount, ClusterRoles и Deployment:

[simterm]

$ kubectl apply -n argo-rollouts -f https://raw.githubusercontent.com/argoproj/argo-rollouts/stable/manifests/install.yaml

[/simterm]

Позже, когда будем внедрять его, используем Argo Rollouts Helm-чарт.

Проверяем под – это Argo Rollouts контроллер:

[simterm]

$ kk -n argo-rollouts get pod
NAME                             READY   STATUS    RESTARTS   AGE
argo-rollouts-6ffd56b9d6-7h65n   1/1     Running   0          30s

[/simterm]

kubectl плагин

Устанавливаем плагин для kubectl:

[simterm]

$ curl -LO https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64
$ chmod +x ./kubectl-argo-rollouts-linux-amd64
$ sudo mv ./kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts

[/simterm]

Проверяем:

[simterm]

$ kubectl argo rollouts version
kubectl-argo-rollouts: v1.0.0+912d3ac
  BuildDate: 2021-05-19T23:56:53Z
  GitCommit: 912d3ac0097a5fc24932ceee532aa18bcc79944d
  GitTreeState: clean
  GoVersion: go1.16.3
  Compiler: gc
  Platform: linux/amd64

[/simterm]

Деплой приложения

Задеплоим тестовое приложение:

[simterm]

$ kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-rollouts/master/docs/getting-started/basic/rollout.yaml
rollout.argoproj.io/rollouts-demo created

[/simterm]

Проверяем:

[simterm]

$ kubectl get rollouts rollouts-demo -o yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
...
spec:
  replicas: 5
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      app: rollouts-demo
  strategy:
    canary:
      steps:
      - setWeight: 20
      - pause: {}
      - setWeight: 40
      - pause:
          duration: 10
      - setWeight: 60
      - pause:
          duration: 10
      - setWeight: 80
      - pause:
          duration: 10
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: rollouts-demo
    spec:
      containers:
      - image: argoproj/rollouts-demo:blue
        name: rollouts-demo
        ports:
        - containerPort: 8080
          name: http
          protocol: TCP
...

[/simterm]

Собственно, в spec.strategy описан тип Canary, в котором в несколько шагов выполняется обновление подов: сначала будут заменены 20%, затем пауза для подтверждения, что всё работает, затем апдейт 40-ка процентов подов, пауза в 10 секунд, и так далее.

С помощью плагина можешь добавить --watch, что бы отслеживать изменения real-time:

[simterm]

$ kubectl argo rollouts get rollout rollouts-demo --watch

[/simterm]

Устанавливаем тестовый Service:

[simterm]

$ kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-rollouts/master/docs/getting-started/basic/service.yaml
service/rollouts-demo created

[/simterm]

И выполняем деплой – обновляем версию образа:

[simterm]

$ ectl argo rollouts set image rollouts-demo   rollouts-demo=argoproj/rollouts-demo:yellow

[/simterm]

Проверяем:

Готово.