Одна из целей, которые мы преследуем внедряя 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]
Проверяем:







