Привет, Хабр!
Это вторая часть из серии статей "Учимся разворачивать микросервисы". В предыдущей части мы написали 2 простеньких микросервиса — бекенд и шлюз, и разобрались с тем, как их упаковать в docker-образы. В этой же статье мы будем организовывать оркестрацию наших docker-контейнеров с помощью Kubernetes. Мы последовательно составим конфигурацию для запуска системы в Minikube, а затем адаптируем ее для деплоя в Google Kubernetes Engine.
План серии:
Создание сервисов на Spring Boot, работа с Docker
Ключевые слова: Java 11, Spring Boot, Docker, image optimization
Разработка Kubernetes конфигурации и деплой системы в Google Kubernetes Engine
Ключевые слова: Kubernetes, GKE, resource management, autoscaling, secrets
Создание чарта с помощью Helm 3 для более эффективного управления кластером
Ключевые слова: Helm 3, chart deployment
Настройка Jenkins и пайплайна для автоматической доставки кода в кластер
Ключевые слова: Jenkins configuration, plugins, separate configs repository
Что конкретно мы попытаемся добиться с помощью Kubernetes:
- Репликация. У нас в эксплуатации будет находится несколько контейнеров каждого типа, между которыми будет распределяться трафик. В случае смерти одного из контейнеров, он должен быть заменен новым.
- Автомасштабирование. Если трафик мал, то логично держать меньше реплик, а при всплесках нагрузки автоматически их добавлять.
- Управление ресурсами. Произвольный контейнер не должен иметь возможность использовать все ресурсы и тем самым подорвать работу других контейнеров. Также неплохо было бы ограничить ресурсы для всей системы в целом.
- Плавные обновления. Если мы захотим что-то поменять в контейнерах, например используемый Docker-образ, то Kubernetes должен плавно заменять старые контейнеры на обновленные, тем самым не допуская простоя в работе.
- Распределенность. Система должна работать на произвольном количестве узлов (нод).
Kubernetes — сложный механизм, призванный сделать нашу систему масштабируемой, отказоустойчивой и простой в развертывании. Он включает в себя множество настроек и возможностей. В статье основной акцент будет сделан на создании Kubernetes-конфигурации для конкретного учебного проекта.
Код проекта доступен на GitHub по ссылке.
Среда Kubernetes
Minikube — это удобный инструмент для экспериментов с Kubernetes на локальной машине. Изначально мы создадим конфигурацию для работы именно в этой среде. Далее мы поговорим, какие корректировки нужно внести, чтобы задеплоить систему в GKE. Google Cloud Platform был выбран из-за бесплатных 300$ на эксперименты в первый год. Для приведенной в статье конфигурации стоит использовать кластер из 2+ стандартных машин (n1-standard-1).
Ссылки по настройке среды:
- Установить Minikube
- Создать кластер на GKE
- Подключить к кластеру kubectl (консольный клиент Kubernetes)
Объекты Kubernetes
При работе с Kubernetes инженер описывает желаемое состояние системы через определение объектов и связей между ними. А конкретные действия для достижения нужного состояния оркестратор волен выбирать сам. То есть можно сказать, что настройка носит декларативный характер.
Рассмотрим некоторые объекты Kubernetes:
Namespace — пространство имен. Объекты могут взаимодействовать, только если находятся в одном неймспейсе. С помощью неймспейсов возможно развернуть несколько виртуальных кластеров на одном физическом.
Pod — минимальный юнит развертывания. В большинстве случаев включает в себя один контейнер. Множество настроек пода делегируются непосредственно контейнеру докера, например, управление ресурсами, политики рестартов, управление портами.
ReplicaSet — контроллер, позволяющий создать набор одинаковых подов и работать с ними, как с единой сущностью. Поддерживает нужное количество реплик, при необходимости создавая новые поды или убивая старые. На самом деле в большинстве случаев вы не будете работать с ReplicaSet напрямую — для этого есть Deployment.
Deployment — контроллер развертывания, являющийся абстракцией более высокого уровня над ReplicaSet'ом. Добавляет возможность обновления управляемых подов.
Service — отвечает за сетевое взаимодействие группы подов. В системе обычно существует несколько экземляров одного микросервиса, соответственно каждый из них имеет свой IP-адрес. Количество подов может изменяться, следовательно набор адресов также не постоянен. Другим частям системы для доступа к рассматриваемым подам нужен какой-то статичный адрес, который Service и предоставляет.
Во избежание путаницы здесь и в дальнейшем под словом "сервис" я буду подразумевать именно объект Kubernetes, а не экземпляр приложения.
Существует несколько видов сервисов. Перечисленные ниже типы для простоты понимания можно рассматривать, как матрешку. Каждый последующий оборачивает предыдущий и добавляет некоторые правила маршрутизации. Создавая сервис более высокого уровня, автоматически создаются сервисы нижележащего типа. Типы сервисов:
- ClusterIP — дефолтный тип сервиса. Единая точка доступа к подам по постоянному IP-адресу, доступному только изнутри кластера.
- NodePort — общий IP-адрес подов (полученный из ClusterIP) соединяется с определенным портом всех нод, на которых развернуты обслуживаемые поды. Поды становятся доступны по адресу
<NodeIP>:<NodePort>
. - LoadBalancer — выходной порт NodePort присоединяется к внешнему балансировщику нагрузки, предоставляемому облачным провайдером. Таким образом мы получаем статический внешний IP-адрес для нашего приложения.
Также Kubernetes из коробки предоставляет поддержку DNS внутри кластера, позволяя обращаться к сервису по его имени. Более подробно про сервисы можно почитать тут.
ConfigMap — объект с произвольными конфигурациями, которые могут, например, быть переданы в контейнеры через переменные среды.
Secret — объект с некой конфиденциальной информацией. Секреты могут быть файлами (№ SSL-сертификатами), которые монтируются к контейнеру, либо же base64-закодированными строками, передающимися через те же переменные среды. В статье будут рассмотрены только строковые секреты.
HorizontalPodAutoscaler — объект, предназначенный для автоматического изменения количества подов в зависимости от их загруженности.
Minikube configuration
Namespace:
Создадим неймспейс:
kubectl create namespace msvc-ns
Установим его как текущий:
kubectl config set-context --current --namespace=msvc-ns
Далее все объекты будут создаваться в неймспейсе 'msvc-ns'. Если этот шаг пропустить, то будет использоваться неймспейс 'default'.
Обычно конфигурация для Kubernetes представляет собой обычный yaml-файл с описанием объектов, но также есть возможность создавать эти объекты и через CLI. В дальнейшем все объекты будут описываться в yaml-формате.
ConfigMap
Объект, предоставляющий свойства для подов. В нашем случае шлюзу для связи с подами бекенда необходимо знать URL-адрес их сервиса. Наш ConfigMap будет содержать адреса внутренних сервисов нашего кластера, и его можно будет заинжектить во все заинтересованные микросервисы (в нашей системе это только шлюз).
apiVersion: v1
kind: ConfigMap
metadata:
name: urls-config
data:
BACKEND_URL: "http://backend:8080/"
Как я говорил ранее, в кластере Kubernetes сервисы доступны по их именам. Как мы увидим далее, сервис бекенда будет иметь имя 'backend' и использовать 8080 порт.
Secret
apiVersion: v1
kind: Secret
metadata:
name: msvc-secret
type: Opaque
stringData:
secret: secret
Тип Opaque подразумевает, то что секрет задается парами ключ-значение. Для особых секретов, например, паролей реестров Docker-образов, существуют отдельные типы. В данном конфиге мы указываем пароль в открытом виде в блоке stringData. Так как секреты хранятся в кодировке base64, то наши данные будут закодированы автоматически. Секрет можно указать в уже закодированном виде:
data:
secret: c2VjcmV0
Deployments
У нас будет два деплоймента — для бекенда и шлюза.
Деплоймент шлюза:
apiVersion: apps/v1
kind: Deployment
metadata:
name: gateway
labels:
tier: gateway
app: microservices
spec:
replicas: 3
selector:
matchLabels:
tier: gateway
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
tier: gateway
spec:
containers:
- name: gateway
image: anshelen/microservices-gateway:latest
envFrom:
- configMapRef:
name: urls-config
env:
- name: SECRET
valueFrom:
secretKeyRef:
name: msvc-secret
key: secret
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 5
periodSeconds: 3
ports:
- containerPort: 8080
protocol: TCP
resources:
limits:
memory: "256Mi"
cpu: "200m"
requests:
memory: "128Mi"
cpu: "50m"
metadata.labels
Эти поля служат для настройки связей между объектами, а также для их уникальной идентификации. В нашем случае мы отмечаем, что наш деплоймент принадлежит приложению с названием 'microservices' и слою 'gateway'.
Дополнительно хочется отметить похожий по смыслу блок metadata.annotations — он используются исключительно для предоставления метаинформации, которая может быть интроспектирована внешними инструментами.
spec.replicas
В этом поле задается количество реплик нашего микросервиса.
spec.selector.matchLabels
Этот элемент устанавливает связь между деплойментом и управляемыми подами. Так в нашем случае деплоймент будет управлять только теми подами, у которых есть метка tier, равная 'backend'. Далее в spec.template мы зададим шаблон для создания подов, причем для корректной работы у каждого из них в поле metadata.labels должна быть та же метка, что и здесь.
spec.strategy
Блок spec.strategy описывает стратегию обновления подов. Тип 'rollingUpdate' подразумевает, что будет создан новый ReplicaSet, старые поды постепенно будут удаляться из старого ReplicaSet'а, а обновленные добавляться в новый. Скорость замены подов (максимальное количество добавляемых/удаляемых реплик от нужного количества) можно регулировать параметрами maxSurge и maxUnavailable. Эта стратегия позволяет плавно обновить деплоймент, избежав даунтайма. В данном контексте блок spec.strategy приведен только в демонстрационных целях, так как полностью совпадает с дефолтным значением.
spec.templates
Блок spec.templates содержит информацию о создаваемых деплойментом подах.
spec.templates.metadata.labels
Как уже было сказано выше, это поле должно коррелировать с spec.selector.matchLabels, чтобы создаваемые поды могли быть "подхвачены" деплойментом.
spec.templates.spec.containers.image
Используемый образ. Тег latest будет присвоен Docker-образу, если мы его запушим в реестр, явно не указав другой тег. Хоть по смыслу этот тег и обозначает самый свежий образ, однако его использование — не лучшая практика в Kubernetes. В случае чего мы не сможем откатиться к предыдущей версии пода, если его образ был перезаписан в реестре. Как минимум поэтому лучше использовать образы с уникальными тегами. Сейчас же мы умышленно используем 'latest' образ и поправим это в 4 части этого цикла статей, когда будем настраивать пайплайн в Jenkins.
spec.templates.spec.containers.envFrom.configMapRef
Ссылаемся на уже созданный ConfigMap и помещаем все значения из него в переменные среды.
spec.templates.spec.containers.env
В этом блоке создаем переменную среды 'SECRET', равную значению из нашего объекта-секрета под ключом 'secret'.
spec.templates.spec.containers.readinessProbe
Проверка готовности пода принимать трафик. Здесь мы указываем эндпойнт, предоставляющий информацию о состоянии микросервиса. Kubernetes будет периодически делать запросы на этот адрес, и если 3 раза подряд статус ответа будет не 200, то проблемный под будет исключен из балансировки нагрузки.
initialDelaySeconds — задежка перед первой проверкой.
periodSeconds — интервал между проверками.
Также существует проверка жизнеспособности пода livenessProbe, но я сомневаюсь в рациональности ее применения здесь (хорошая статья на эту тему).
spec.templates.spec.containers.ports
В блоке ports мы сообщаем какие порты у контейнера открыть. Эта настройка делегируется докеру (аналогично указанию при запуске контейнера параметра-p 8080:8080
).
spec.templates.spec.containers.resources
Ограничения контейнера по ресурсам. limits — максимально доступное количество, а requests — количество ресурсов, предоставляемое контейнеру единовременно при старте. 200m — 200 миллиядер (одна пятая ядра), Mi — мегабайты.
Деплоймент микросервиса бекенда имеет аналогичную конфигурацию, за исключением того, что мы не должны сообщать ему секрет через переменную среды.
Services
Сервис бекенда:
apiVersion: v1
kind: Service
metadata:
labels:
tier: backend
name: backend
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 8080
selector:
tier: backend
spec.ports.targetPort — порт облуживаемых подов, spec.ports.port — выходной порт сервиса. В spec.selector мы указываем, что этот сервис будет направлять запросы подам, имеющим метку tier, равную 'backend'. Так как тип сервиса не указан явно, то он считается равным ClusterIP, и сервис доступен напрямую только изнутри кластера по адресу http://backend:8080
.
Сервис шлюза:
apiVersion: v1
kind: Service
metadata:
labels:
tier: gateway
name: gateway
spec:
ports:
- nodePort: 30500
port: 80
protocol: TCP
targetPort: 8080
selector:
tier: gateway
type: NodePort
Так как мы работаем с Minikube, и у нас нет внешнего балансировщика нагрузки, то выберем тип сервиса NodePort. spec.ports.nodePort — порт на хосте. Если его не указать, то будет выбран рандомный порт из интервала 30000-32767.
Итоговый файл конфигурации для Minikube
apiVersion: v1
kind: ConfigMap
metadata:
name: urls-config
data:
BACKEND_URL: "http://backend:8080/"
---
apiVersion: v1
kind: Secret
metadata:
name: msvc-secret
type: Opaque
stringData:
secret: secret
---
apiVersion: v1
kind: Service
metadata:
labels:
tier: backend
name: backend
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 8080
selector:
tier: backend
---
apiVersion: v1
kind: Service
metadata:
labels:
tier: gateway
name: gateway
spec:
ports:
- nodePort: 30500
port: 80
protocol: TCP
targetPort: 8080
selector:
tier: gateway
type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
labels:
tier: backend
app: microservices
spec:
replicas: 3
selector:
matchLabels:
tier: backend
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
tier: backend
spec:
containers:
- name: backend
image: anshelen/microservices-backend:latest
envFrom:
- configMapRef:
name: urls-config
ports:
- containerPort: 8080
protocol: TCP
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 5
periodSeconds: 3
resources:
limits:
memory: "256Mi"
cpu: "200m"
requests:
memory: "128Mi"
cpu: "50m"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gateway
labels:
tier: gateway
app: microservices
spec:
replicas: 3
selector:
matchLabels:
tier: gateway
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
tier: gateway
spec:
containers:
- name: gateway
image: anshelen/microservices-gateway:latest
envFrom:
- configMapRef:
name: urls-config
env:
- name: SECRET
valueFrom:
secretKeyRef:
name: msvc-secret
key: secret
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 5
periodSeconds: 3
ports:
- containerPort: 8080
protocol: TCP
resources:
limits:
memory: "256Mi"
cpu: "200m"
requests:
memory: "128Mi"
cpu: "50m"
Запуск
Применяем конфигурацию:
kubectl apply -f deploy.yaml
Подождем пока все объекты Kubernetes запустятся и получим URL нашего приложения:
minikube service gateway --url -n msvc-ns
Далее сгенерируем трафик:
for i in `seq 50`; do curl $(minikube service gateway --url -n msvc-ns) && echo; done
Вывод команды (будет отличаться в вашем случае):
Number of requests 1 (gateway 544429797, secret secret)
Number of requests 1 (gateway 1543772618, secret secret)
Number of requests 2 (gateway 544429797, secret secret)
Number of requests 3 (gateway 544429797, secret secret)
Number of requests 4 (gateway 544429797, secret secret)
Number of requests 1 (gateway -1940767433, secret secret)
Number of requests 2 (gateway -1940767433, secret secret)
Number of requests 2 (gateway 1543772618, secret secret)
Number of requests 5 (gateway 544429797, secret secret)
...
Number of requests 1 (gateway 544429797, secret secret)
Number of requests 1 (gateway 1543772618, secret secret)
Number of requests 2 (gateway 544429797, secret secret)
Number of requests 3 (gateway 544429797, secret secret)
Number of requests 4 (gateway 544429797, secret secret)
Number of requests 1 (gateway -1940767433, secret secret)
Number of requests 2 (gateway -1940767433, secret secret)
Number of requests 2 (gateway 1543772618, secret secret)
Number of requests 5 (gateway 544429797, secret secret)
Number of requests 3 (gateway 1543772618, secret secret)
Number of requests 6 (gateway 544429797, secret secret)
Number of requests 3 (gateway -1940767433, secret secret)
Number of requests 4 (gateway 1543772618, secret secret)
Number of requests 7 (gateway 544429797, secret secret)
Number of requests 4 (gateway -1940767433, secret secret)
Number of requests 8 (gateway 544429797, secret secret)
Number of requests 9 (gateway 544429797, secret secret)
Number of requests 10 (gateway 544429797, secret secret)
Number of requests 5 (gateway 1543772618, secret secret)
Number of requests 5 (gateway -1940767433, secret secret)
Number of requests 6 (gateway -1940767433, secret secret)
Number of requests 7 (gateway -1940767433, secret secret)
Number of requests 6 (gateway 1543772618, secret secret)
Number of requests 8 (gateway -1940767433, secret secret)
Number of requests 7 (gateway 1543772618, secret secret)
Number of requests 11 (gateway 544429797, secret secret)
Number of requests 12 (gateway 544429797, secret secret)
Number of requests 8 (gateway 1543772618, secret secret)
Number of requests 9 (gateway -1940767433, secret secret)
Number of requests 10 (gateway -1940767433, secret secret)
Number of requests 11 (gateway -1940767433, secret secret)
Number of requests 9 (gateway 1543772618, secret secret)
Number of requests 10 (gateway 1543772618, secret secret)
Number of requests 11 (gateway 1543772618, secret secret)
Number of requests 12 (gateway -1940767433, secret secret)
Number of requests 12 (gateway 1543772618, secret secret)
Number of requests 13 (gateway 544429797, secret secret)
Number of requests 13 (gateway 1543772618, secret secret)
Number of requests 13 (gateway -1940767433, secret secret)
Number of requests 14 (gateway 1543772618, secret secret)
Number of requests 14 (gateway -1940767433, secret secret)
Number of requests 15 (gateway -1940767433, secret secret)
Number of requests 14 (gateway 544429797, secret secret)
Number of requests 15 (gateway 544429797, secret secret)
Number of requests 16 (gateway 544429797, secret secret)
Number of requests 17 (gateway 544429797, secret secret)
Number of requests 15 (gateway 1543772618, secret secret)
Number of requests 16 (gateway 1543772618, secret secret)
Number of requests 16 (gateway -1940767433, secret secret)
Number of requests 17 (gateway 1543772618, secret secret)
Запросы поступают бекендам равномерно, и каждый бекенд принимает запросы именно от своего шлюза. Признаюсь, я несколько раз проводил запуск, чтобы получить такую картину. Если запустить команду повторно через какое-то время, то шлюзы и бекенды могут быть связаны уже по-другому, причем есть вероятность, что все шлюзы будут слать запросы одному бекенду. Это связано с тем, что имеет место балансировка между клиентами, а не запросами. Например, если один клиент будет слать 1 запрос в секунду, а другой 1000, и они будут изначально "привязаны" к разным репликам, то это приведет к разнице в нагрузке в 1000 раз. Это, безусловно, не лучший вариант балансировки трафика, однако дальнейшее исследование этой темы оставим за рамками данной статьи. Подробнее можно прочитать здесь.
Управление кластером
Здесь я перечислю несколько полезных команд для управления кластером.
Извлечение информации
kubectl get <object-type>
— вывести список объектов определенного типа. В качестве типов может выступать 'pod', 'service', 'deployment' и другие. Чтобы посмотреть все объекты в неймспейсе подставьте 'all'.
kubectl get <object-type> <object-name> -o yaml
— выведет полную конфигурацию объекта в yaml-формате.
kubectl describe <object-type> <object-name>
— подробная информация об объекте.
kubectl cluster-info
— информация о кластере.
kubectl top pod/node
— потребляемые ресурсы подами/нодами.
Изменение конфигурации кластера
kubectl apply -f <file/directory>
— применить конфигурационный файл или же все файлы из директории.
kubectl delete <object-type> <object-name>
— удалить объект.
kubectl scale deployment <deployment-name> --replicas=n
— отмасштабировать деплоймент. Если выполнить подряд две команды: с n = 0, а затем с другим n, то пересоздаст все поды в деплойменте.
kubectl edit <object-type> <object-name>
— редактировать конфигурацию объекта в редакторе.
kubectl rollout undo deployment <deployment-name>
— откатить изменения деплоймента до прошлой версии.
Дебаг
kubectl logs <pod-name>
— отобразить логи контейнера. Параметр -f
позволит следить за логами в реальном времени.
kubectl port-forward <pod-name> <host-port>:<container-port>
— пробросить порт хоста на порт контейнера. Используется для отправки запросов подам вручную.
kubectl exec -it <pod-name> -- /bin/sh
— открыть терминал в контейнере пода.
kubectl run curl --image=radial/busyboxplus:curl -i --tty
— создать отдельный под. В данном примере создается легкий контейнер с curl, с помощью которого можно будет, например, проверить доступность сервисов.
kubectl get events --sort-by='.metadata.creationTimestamp'
— получить список внутренних событий Kubernetes. Эта информация поможет ответить, например, на вопрос, почему под не смог запуститься.
GKE configuration
Как я уже говорил, далее мы обсудим, как можно изменить конфигурацию, чтобы задеплоить систему в Google Kubernetes Engine.
Services
Инфраструктура GKE предоставляет нам внешний балансировщик нагрузки, который нам нужен для получения статичного внешнего IP-адреса. Чтобы подключить балансировщик, изменим тип сервиса шлюза на LoadBalancer:
apiVersion: v1
kind: Service
metadata:
labels:
tier: gateway
name: gateway
spec:
selector:
tier: gateway
ports:
- port: 80
protocol: TCP
targetPort: 8080
type: LoadBalancer
Как справедливо заметили в комментариях, при пересоздании балансировщика его статический адрес поменяется, что чаще всего нежелательно. Чтобы этого избежать, надо арендовать статический адрес у GCP (Сеть VPC -> Внешние IP-адреса) в той же зоне, что и кластер, а затем прописать его в элементе spec.loadBalancerIp.
HorizontalPodAutoscalers
HorizontalPodAutoscaler будет автоматически масштабировать деплоймент в зависимости от нагрузки на его поды. Мне не удалось заставить работать этот компонент на Minikube (какие-то странные неполадки с сервером метрик), но в GKE он работает из коробки.
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: backend
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: backend
minReplicas: 1
maxReplicas: 3
targetCPUUtilizationPercentage: 50
В spec.scaleTargetRef мы указываем, что мы собираемся автомасштабировать деплоймент под именем backend. Далее сообщаем, что собираемся содержать от 1 до 3 реплик и планируем держать поды загруженными на 50%. Отмечу, чтобы задавать планируемую загрузку в процентах (можно указывать и в абсолютных величинах), то надо обязательно указать requests.cpu у управляемых контейнеров.
Конфигурация HorizontalPodAutoscaler'а шлюза аналогична.
Quotas
Квоты позволяют настроить максимальное потребление ресурсов всем кластером. Это обычно нужно, если несколько команд используют один кластер (multitenant environment). Давайте ограничим ресурсы, доступные объектам нашего неймспейса:
apiVersion: v1
kind: ResourceQuota
metadata:
name: msvc-quota
spec:
hard:
limits.cpu: "2"
limits.memory: 4Gi
Если мы проставляем жесткие ограничения квот по какому-либо параметру, то для каждого из создаваемых подов этот параметр становится обязательным. Это может вызывать неудобства, например, при создании контейнеров из CLI (см. kubectl run
), поэтому установим дефолтные параметры с помощью объекта LimitRange:
apiVersion: v1
kind: LimitRange
metadata:
name: msvc-default-resources
spec:
limits:
- default:
memory: "512Mi"
cpu: "250m"
defaultRequest:
memory: "256Mi"
cpu: "50m"
type: Container
Так как конфигурация квот напрямую не относится к нашему приложению, то лучше оформить ее в отдельный файл.
Итоговые файлы конфигурации для GKE
Поместим все файлы в папку scripts_gke/
.
apiVersion: v1
kind: ResourceQuota
metadata:
name: msvc-quota
spec:
hard:
limits.cpu: "2"
limits.memory: 4Gi
---
apiVersion: v1
kind: LimitRange
metadata:
name: msvc-default-resources
spec:
limits:
- default:
memory: "512Mi"
cpu: "250m"
defaultRequest:
memory: "256Mi"
cpu: "50m"
type: Container
apiVersion: v1
kind: ConfigMap
metadata:
name: urls-config
data:
BACKEND_URL: "http://backend:8080/"
---
apiVersion: v1
kind: Secret
metadata:
name: msvc-secret
type: Opaque
stringData:
secret: secret
---
apiVersion: v1
kind: Service
metadata:
labels:
tier: backend
name: backend
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 8080
selector:
tier: backend
---
apiVersion: v1
kind: Service
metadata:
labels:
tier: gateway
name: gateway
spec:
selector:
tier: gateway
ports:
- port: 80
protocol: TCP
targetPort: 8080
type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
labels:
tier: backend
app: microservices
spec:
replicas: 2
selector:
matchLabels:
tier: backend
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
tier: backend
spec:
containers:
- name: backend
image: anshelen/microservices-backend:latest
envFrom:
- configMapRef:
name: urls-config
ports:
- containerPort: 8080
protocol: TCP
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 5
periodSeconds: 3
resources:
limits:
memory: "512Mi"
cpu: "250m"
requests:
memory: "256Mi"
cpu: "50m"
---
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: backend
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: backend
minReplicas: 1
maxReplicas: 3
targetCPUUtilizationPercentage: 50
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gateway
labels:
tier: gateway
app: microservices
spec:
replicas: 2
selector:
matchLabels:
tier: gateway
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
tier: gateway
spec:
containers:
- name: gateway
image: anshelen/microservices-gateway:latest
envFrom:
- configMapRef:
name: urls-config
env:
- name: SECRET
valueFrom:
secretKeyRef:
name: msvc-secret
key: secret
readinessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 5
periodSeconds: 3
ports:
- containerPort: 8080
protocol: TCP
resources:
limits:
memory: "512Mi"
cpu: "250m"
requests:
memory: "256Mi"
cpu: "50m"
---
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: gateway
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: gateway
minReplicas: 1
maxReplicas: 3
targetCPUUtilizationPercentage: 50
Запуск
Применим все конфигурационные файлы из папки scripts_gke:
kubectl apply -f scripts_gke/
Время разворачивания в этот раз может составить несколько минут, так как потребуется время на установку внешнего балансировщика. URL нашего приложения:
kubectl get svc gateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
Через некоторое время HorizontalPodAutoscaler'ы в отсутствие нагрузки сократят количество подов в каждом деплойменте до одного.
Заключение
В этой статье мы написали Kubernetes-конфигурацию и успешно задеплоили нашу систему в Google Kubernetes Engine.
Возможно, когда вы читали эту статью, то заметили, что даже для деплоя простенькой системы из двух микросервисов очень многое надо держать в голове. Для установки корректной связи между объектами одну и ту же метку надо не забыть прописать в нескольких местах, при описании сервисов и подов надо не запутаться в их портах… Было бы неплохо подключить какой-нибудь шаблонизатор и получить возможность гибко управлять настройками нашей системы в целом, обособившись от абстракций Kubernetes. Все это (и много больше) позволяет сделать пакетный менеджер Helm.
В третьей части этого цикла статей мы потрогаем Helm 3, создадим helm-чарт для нашей системы и выложим его в репозиторий, созданный на основе GitHub pages.
Akuma
Из собственного опыта:
1. Minikube — внезапно проблемное нечто. Кроме внезапных и малопонятных глюков, как например в статье, если использовать его в виртуалке, то можно обнаружить, что ФС полностью очищается при перезапуске. И если вы решите поставить в эту же виртуалку что-то «внешнее» — будете страдать.
2. Почему-то во всех гайдах про LoadBalancer забывают одну важную деталь. Если кто-то решит использовать такой yaml на реальном проекте, он может обнаружить, что при пересоздании LoadBalancer (не важно по каким причинам) IP поменяется. А все потому что надо в GKE отдельно арендовать IP и к нему привязывать LoadBalancer. Это просто, но надо знать заранее.
VolCh
1 характерно и для управляемых кластеров, даже если дали права на саму ноду. Там какой-нибудь паппет может по крону все очишать. Так что близко к реальности.
Akuma
Согласен. Но для локальной разработки — неудобно. У меня в итоге обычная виртуалка крутится, на которой обычный k8s поднят. Так проще.
buldezir
а какой вообще самый простой способ экспериментировать с k8s на одной ноде? (ну то есть на одной виртуалке или 1vps)
Akuma
Rancher?
VolCh
Тут спорно. Удобно, конечно локально делать всё, что хочешь, но потом это рано или поздно приводит к ошибкам при "у меня всё работает" и необходимости дебажить и фиксить хорошо если на тестовом окружении удаленном, а не на проде.
Akuma
Если вы подскажете способ очень быстрой (как у шареных папок в виртуалбоксе) синхронизации файлов исходников с удаленным k8s, буду благодарен.
У меня такого способа увы, нет. А пушить, собирать, ждать деплоя при изменении одной строчки мне не нравится. Поэтому виртуалка.
В реальности же практически не сталкиваюсь с проблемами «у меня все работает» при условии использовании одного и того же дистрибутива. Плюс стараюсь делать минимальные отличия дев/прод. Т.е. это буквально выделение ресурсов, реплики и докер-образ с вшитыми исходниками.
UPD. Это накладывает некоторые технические особенности. Например, я работаю на ноуте с 32Гб озу, чтобы все это влазило локально. Кто-то скажет, что это дофига.
VolCh
telepresence попробуйте
Akuma
Либо я не понял что куда, либо это не то.
Проще уж виртуалку поднять, чем разворачивать часть там, часть тут.
VolCh
Оно подставляет прокси на локальную машину вместо целевого контейнера в любом кластере (при наличии прав, конечно), хоть локально, хоть в виртуалке, хоть в каком-нибудь клауде на продакшене. Локально же поднимается произвольный докер-контейнер (есть ещё какие-то варианты, но не вникал) с монтированием произвольных каталогов.
То есть это синхронизация файлов с удаленным кластеров, а подмена удаленных файлов и процессов в контейнере кластера своими. Единственный вариант, в котором без танцев с бубнами у меня заработали все основные функции вебпака с конфигом от create-react-app, включая горячую перезагрузку при изменении исходников.
Akuma
Значит я все правильно понял. По мне так локальная виртуалка удобнее. По крайней мере в моем случае. Плюс это бесплатно — не нужно держать отдельную дев-машину (мне например надо 16 озу + нормальный проц).
Под какие-то другие проекты, возможно, будет удобнее. Приму на вооружение, спасибо.
gecube
https://github.com/Skalar/k8sync?
то еще говно на самом деле
Anshelen Автор
Спасибо за комментарий. Согласен, этот момент с LoadBalancer'ом стоит упомянуть. Дополнил статью