Одна из целей, которые мы преследуем внедряя ArgoCD в Kubernetes – использование новых Deployment Strategies для наших приложений.
Ниже рассмотрим типы деплойментов в Kubernetes, как работают Deployment в Kubernetes, и быстрый пример использования Argo Rollouts, который более детально будем рассматривать в следущих постах.
Содержание
Deployment Strategies и Kubernetes
Кратко рассмотрим стратегии деплоя, имеющиеся в Kubernetes.
Сам Kubernetes “из коробки” предоставляет два типа .spec.strategy.type
– Recreate и 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 в свою очередь, создаёт новые поды:
Каждый под, созданный с помощью 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
Устанавливаем тестовый 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]
Проверяем: