Привет, Хабр!


Это вторая часть из серии статей "Учимся разворачивать микросервисы". В предыдущей части мы написали 2 простеньких микросервиса — бекенд и шлюз, и разобрались с тем, как их упаковать в docker-образы. В этой же статье мы будем организовывать оркестрацию наших docker-контейнеров с помощью Kubernetes. Мы последовательно составим конфигурацию для запуска системы в Minikube, а затем адаптируем ее для деплоя в Google Kubernetes Engine.


План серии:


  1. Создание сервисов на Spring Boot, работа с Docker


    Ключевые слова: Java 11, Spring Boot, Docker, image optimization


  2. Разработка Kubernetes конфигурации и деплой системы в Google Kubernetes Engine


    Ключевые слова: Kubernetes, GKE, resource management, autoscaling, secrets


  3. Создание чарта с помощью Helm 3 для более эффективного управления кластером


    Ключевые слова: Helm 3, chart deployment


  4. Настройка 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).


Ссылки по настройке среды:



Объекты 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

deploy.yaml
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/.


create_quotas.yaml
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

deploy.yaml
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.