Как развернуть микросервис в Kubernetes — Deployment, Service, Configmap
Развертывание приложений в Kubernetes рассмотрим на примере двух взаимодействующих микросервисов, Первый, который называется service-back — внутренний и он будет доступен только внутри кластера. Второй назовем service-front и он будет вызываться снаружи по https. Внешний и внутренний сервисы предостаставляют REST API для поиска данных о сотрудниках компании по первым буквам ФИО, либо табельному номеру.

Запрос на поиск одного или нескольких сотрудников по первым буквам фамилии поступает в в service-front через Ingress, далее маршрутизируется на один из подов приложения service-front. Для каждого элемента массива в запросе для service-front делается отдельный запрос в service-back. Отметим, что такая архитектура взаимодействия сделана просто в качестве тестового примера и не является оптимальной
Основные конфигурационные файлы, необходимые для развертывания приложения в Kubernetes, мы рассмотрим на примере внутреннего сервиса service-back, а конфигурации для service-front будут отличаться только настройкой доступа к нему из-за пределов кластера Kubernetes и будут рассмотрены в других статьях. Тем не менее, проверить вызов service-back мы сможем, прокинув его порт на localhost и опишем этот момент в конце материала.
Генерация образа с помощью jib
После написания программного кода приложения его необходимо упаковать в docker образ, разместить этот образ в репозитории и указать ссылку на него внутри Deployment файла приложения.
Исходный образ, на основе которого далее будет создан образ приложения service-back, можно сделать самостоятельно или взять один из готовых образов из репозитория dockerhub, а потом уже залить в dockerhub готовый образ приложения.
Шаг 1. Регистрируемся на сайте https://hub.docker.com/ и создаем свой репозиторий. Добавленные пользователем образы будут доступны через url <имя_пользователя>/<имя_репозитория>:<имя_образа>:<тэг(версия)>. Один образ залитый в репозиторий можем иметь несколько версий (тэгов). Если версия не указана или в качестве версии указано latest, то это будет означать ссылку на последнюю версию образа. Но, в примере мы используем другой подход, когда версия указывается в имени образа, таким образом url образа получается следующим:
lokrusta/lokrusta-repo/service-back-0.01-SNAPSHOT
Где lokrusta – имя пользователя dockerhub, lokrusta-repo – имя репозитория пользователя, service-back – имя приложения, 0.01-SNAPSHOT – версия приложения
Возникает вопрос, а где же в ссылке на образ URL хоста репозитория и как авторизоваться в этом репозитории, если он приватный? Дело в том, что по-умолчанию, когда URL хоста не указан, kubernetes считает, что образ лежит на dockerhub и в качестве хоста берет http://docker.io/. О том как указать данные для авторизации в репозитории, если он приватный, расскажем ниже <не забыть!>. Пока же мы получили представление о том где хранятся образы и как указывается путь к ним.
Шаг 2. Выбираем базовый образ для нашего java приложения. На dockerhub хранится большое количество готовых образов, в том числе образы, содержащие jre и jdk. Нам лучше выбрать тот, что по максимуму содержит java утилиты, которые мы потом сможем использовать для отладки и диагностики приложения внутри контейнера. Для этих целей подойдет openjdk:17-jdk-oracle (ссылка).
Нужно отметить, что предпочтительнее было бы создать самим исходный образ, включив в него все необходимые нам утилиты. В частности, нам пригодились бы разные варианты команды top, curl и.т.д. Но для простоты мы пока в качестве базового возьмем готовый.
Шаг 3. Добавляем в скрипт сборки новую задачу prepareDockerImageBuild для генерации образа нашего приложения с помощью jib. С помощью jib-плагина мы можем собрать образ нашего java приложения на основе уже существующего. Ниже приведем скрипт задачи сборки для gradle:
task prepareDockerImageBuild {
doFirst {
jib {
from {
image = 'openjdk:17-jdk-oracle'
auth {
username = findProperty('DOCKER_USER')
password = findProperty('DOCKER_PASSWORD')
}
}
container {
jvmFlags = ['-Dfile.encoding=UTF-8', '-Dspring.config.location=file:/var/app/config/application.yml']
format = 'Docker'
mainClass = project.ext.applicationMainClass
appRoot = '/opt/app'
}
to {
image = "lokrusta/lokrusta-repo:${project.name}-${applicationVersion}"
auth {
username = findProperty('DOCKER_USER')
password = findProperty('DOCKER_PASSWORD')
}
}
}
}
}
Чтобы скрипт работал нужно в settings.gradle указать версию плагина:
pluginManagement {
plugins {
id 'org.springframework.boot' version "${springBootVersion}"
id 'com.google.cloud.tools.jib' version "${jibVersion}"
id 'org.openapi.generator' version "${openApiVersion}"
}
}
а в build.gradle использовать сам плагин:
plugins {
id 'org.openapi.generator'
id 'org.springframework.boot'
id "com.google.cloud.tools.jib"
}
После добавления автоматически в цикл сборки будет включена задача jib, но нам надо еще указать зависимость задачи jib от задач prepareDockerImageBuild и springBootJar:
tasks.jib.dependsOn(prepareDockerFrontImageBuild, bootJar)
Если подробно рассмотреть код задачи prepareDockerImageBuild, то мы увидим в нем 3 раздела:
Исходный образ и данные для авторизации на dockerhub (интересен факт, что для скачивания образа из public-репозитория kubernetes-ом авторизация не нужна, но для работы jib-плагина авторизоваться на dockerhub все же требуется, так как нам нужно не только скачать образ, но и залить новый образ в репозиторий)
Данные о параметрах создаваемого контейнера — тип (docker), папка в контейнере где будет лежать готовое приложение (opt/app), имя основного класса приложения и дополнительные параметры запуска. Ниже мы увидим, что команду запуска для нашего приложения можно по-умолчанию не указывать <не забыть!>, но тогда нам обязательно надо указать необходимые параметры запуска в jvmFlags при сборке образа. В данном случае, в jvmFlags мы указываем откуда брать файл настроек application.yml и кодировку файлов по-умолчанию
Третий раздел задает куда необходимо положить создаваемый образ
Создаем Deployment
Основным манифестом Kubernetes, который описывает как разместить микросервис в Kubernetes является Deployment. Нужно отметить, что это далеко не единственный вариант и в приведенной ниже таблице мы кратко представили и другие способы с указанием, когда они применяются:
Deployment | Основной вариант описания пользовательских микросервисов в Kubernetes |
StatefulSet | Отличается от Deployment тем, что каждый экземпляр приложения имеет уникальный идентификатор и экземпляры не взаимозаменяемы. Используется при размещении в кластере баз данных, систем сбора метрик итд |
Daemon | Так размещают служебные микросервисы, когда требуется один экземпляр на ноду. Подходит, например, для развертывания служб сбора логов, таких как fluent-bit |
Job | Служит для разового запуска приложения, то есть приложение запускается, выполняет свою работу и завершается. Подходит для генераторов нагрузки, например, k6 |
ReplicaSet | Абстракция более низкого уровня, чем Deployment. Описывает набор реплик микросервиса и неявно создается при объявлении Deployment. Напрямую применять не рекомендуется |
Pod | Самый простой способ быстро и разово развернуть сервис в Kubernetes, но не подходит для управления размещением, так как после удаления пода вся информация о развертывании сервиса теряется. Неявно создается более высокими абстракциями. Напрямую применять не рекомендуется |
Ниже приведен рабочий вариант манифеста Deployment для микросервиса service-back:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: service-back
version: "0.01"
group: service-back
name: service-back-0.01
namespace: myproject
spec:
replicas: 1
selector:
matchLabels:
app: service-back
version: "0.01"
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 25%
maxSurge: 100%
template:
metadata:
labels:
app: service-back
version: "0.01"
spec:
containers:
- name: service-back
image: "lokrusta/lokrusta-repo:service-back-0.01-SNAPSHOT"
imagePullPolicy: Always
workingDir: /opt/app
command: [ 'java','-Dspring.config.location=file:/var/app/config/application.yml','-Dfile.encoding=UTF-8', '-cp', '@/opt/app/jib-classpath-file', '@/opt/app/jib-main-class-file', '-Xms256m', '-Xmx512m' ]
ports:
- containerPort: 8080
protocol: TCP
- containerPort: 8081
protocol: TCP
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8081
initialDelaySeconds: 10
timeoutSeconds: 5
periodSeconds: 10
successThreshold: 1
failureThreshold: 6
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8081
initialDelaySeconds: 10
timeoutSeconds: 5
periodSeconds: 5
successThreshold: 1
failureThreshold: 6
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 1000m
memory: 1Gi
volumeMounts:
- name: dump-volume
mountPath: /dump
- name: config
mountPath: /var/app/config/application.yml
subPath: application.yml
- name: log
mountPath: /opt/app/log
- name: logging-app
image: busybox:1.28
workingDir: /opt/app/log
args: [ /bin/sh, -c, 'tail -n+1 -F application.log' ]
imagePullPolicy: Always
volumeMounts:
- name: log
mountPath: /opt/app/log
volumes:
- name: dump-volume
hostPath:
path: /var/tmp
type: Directory
- name: config
configMap:
name: service-back-cm-0.01
- name: log
emptyDir: {}
Рассмотрим все поля в Deployment подробно
Первый строчкой указано API, которое используется при указании атрибутов Deployment. Разные варианты API поддерживают разные наборы типов манифестов Kubernetes. А тип манифеста и API можно сравнить со схемой в XML-документе — они определяют набор, тип и значения других полей. Поддержка дополнительных типов манифестов (Custom Resource Definitions) реализуется через специальное API для описания расширений и устанавливается отдельно, в то время как базовые типы поставляются в составе Kubernetes изначально.
Далее указан тип конфигурации в манифесте. Это Deployment
Метки, пространство и управление масштабированием
В разделе metadata указаны метки нашего приложения, его имя и пространство где оно должно размещаться. Метки и имя приложения важны для ссылки на эти атрибуты в других конфигурациях, чтобы можно было указать к каким ресурсам эти конфигурации применяются. Пространство или нэймспейс можно указать в самом Deployment или задавать его параметром в kubectl:
-n <имя_пространства>
чтоб указать к какому пространству данная команда применяется
В блоке spec указано 3 раздела — replicas, selector.matchLabels и strategy. Через replicas указано количество подов, которое необходимо создать для данного Deployment. Пока мы указали самый простой вариант с одним подом, но для гибкого масштабирования можно указывать атрибуты maxReplicas и minReplicas, а количество реплик может меняться динамически через HorizontalPodAutoScaler в зависимости от утилизации ресурсов на подах.
В блоке selector.matchLabels повторно указываются метки, но эти метки относятся к подам, в то время как метки в блоке metadata относились к самому Deployment. Ниже в блоке template.metadata.labels метки указываются в третий раз. Это может путать, но главное знать, что метки в selector.matchLabels и template.metadata.labels должны совпадать и по ним Kubernetes будет искать нужные поды, а метки в разделе metadata нужны, чтоб по ним фильтровать поиск нужных манифестов в web консоли Kubernetes.
Управление обновлениями
Далее в блоке strategy описывается способ обновления подов при выпуске новый версий сервиса и перенакате Deployment-а. Перенакат Deployment может быть связан не только с изменением конфигурации внутри Deployment, а и с изменением самого образа. Тип обновления RollingUpdate указывает на то, что необходимо постепенно обновлять поды и, если очередной обновленный под не пройдет проверки живучести, то процесс обновления надо откатить. В настройках maxUnavailable и maxSurge указано на сколько можно уменьшить и увеличить количество доступных подов сервиса при обновлении. При указанных настройках обновление будет происходить следующим образом:
— будет создан еще один под service-back
— новый под должен пройти проверки liveness и readiness
— на новый под перенаправляется весь трафик
— старый под выводится из под нагрузки, но еще некоторое время работает после того, как на него перестает перенаправляться трафик
Такая схема обновления безопасна с точки зрения минимизации потерь клиентских запросов и обеспечения доступности, однако и при таком подходе может, что-то пойти не так. Например, под уже выведенный из под нагрузки очень долго выполняет клиентский запрос и таймаута, после которого старый под останавливается, не хватило. Также надо понимать, что при обновлении параллельно будут работать 2 версии приложения. Версия n и версия n+1. Соответственно, новая версия должна обязательно быть обратно совместима со старой. Но и это еще не все. Что если обновление произошло успешно, но из-за ошибки, допущенной в программном коде обновленное приложение не работает как надо. А ведь уже весь трафик переключен на новую версию и 100% пользователей начинают получать ошибки. Чтоб решить эту проблему используются канареечные обновления и A/B тестирование и об этом будет рассказано в других статьях. А пока идем дальше
Образ приложения и команда запуска
Пожалуй, самый главный элемент в конфигурации Deployment, это массив template.spec.containers, где описываются какие контейнеры должны содержаться в поде и их свойства. Мы рассмотрим в данной статье только главный контейнер — service-back. Дополнительный контейнер для логирования и его функции будут описаны в других статьях. У каждого контейнера есть имя и ссылка на образ. Если образ содержится в приватном репозитории, то для доступа к нему используются ссылки на секреты, указываемые в разделе imagePullSecrets, но в нашем примере это не нужно. Важным атрибутом является стратегия imagePullPolicy, которая задает в каких случаях нужно перезакачивать образ из репозитория. Значение Allways говорит о том, что надо перезаливать образ всегда и если репозиторий будет недоступен, то поды не обновятся.
При создании образа контейнера мы указали рабочую папку приложения, ее же указываем в поле workingDir.
Далее следует необязательный атрибут command, где задается команда запуска нашего приложения, но мы могли бы обойтись и без нее, достаточно только было бы указать настройки java в переменной окружения JAVA_ENV_OPTIONS. Но есть нюанс, что JAVA_ENV_OPTIONS применялись бы к запуску любой java утилиты на поде (например, к jstack или jmap), а параметры указанные в команде запуска (-Xms и проч) применяются только к запуску нашего основного приложения.
Стоит также обратить внимание, что в команде запуска имя исполняемого файла и путь к библиотекам указывают на имена файлов, в которых указана соответствующая информация, перед которыми стоит символ @:
command: [ 'java','-Dspring.config.location=file:/var/app/config/application.yml','-Dfile.encoding=UTF-8', '-cp', '@/opt/app/jib-classpath-file', '@/opt/app/jib-main-class-file', '-Xms256m', '-Xmx512m' ]
Подробнее пока на этом останавливаться не будем, но если зайти в консоль пода и в папке opt/app/ дать команду:
cat jib-classpath-file
то мы в этом убедимся
Порты основного контейнера
Порты контейнеров в конфигурации Deployment указываются только для информации, чтобы разработчики и администраторы могли ориентироваться в протокола и номерах портов, используемых приложением.
Проверки живучести
В разделах livenessProbe и readinessProbe указываются настройки URL и таймаутов по которым Kubernetes периодически отсылает запросы, чтобы проверить, что приложение работоспособно и готово принимать запросы. Если не проходит readinessProbe, то трафик на под перестает поступать, если не проходит livenessProbe, то Kubernetes пересоздает контейнер. В нашем примере указаны стандартные URL Spring Boot приложения, которые обеспечивает Spring Boot Actuator. Более подробно о проверках живучести, прогревах и о том как снизить вероятность потерь трафика при запросах на неработающие поды мы расскажем в отдельной статье. А в данном примере мы указали, что начинаем проверки живучести через 10 секунд после старта контейнера, повторяем их каждые 5 секунд. В случае хоть одного успешного запроса считаем, что проверка прошла. Если было 6 неуспешных запросов , то считаем, что проверка не прошла.
Ресурсы контейнера
В ресурсах указывается количество оперативной памяти и процессора, которые под запрашивает для контейнера. При этом в requests указываются минимальные требования, а в limits максимальное ограничение. Если limits не задан или совпадает с requests, то запрашиваемое изначально количество ресурсов не может увеличиваться и Kubernetes предпочтет убить под с ошибкой OOMKilled, чем назначит ему дополнительные ресурсы. В нашем примере запрошено 1 ядро процессора и 1G оперативки, при этом приложению мы выделили максимум 512 мегабайт (не считая того, что может быть доступно через использование прямых буферов. Подробнее об этом в статье Все ли мы знаем про OOM Killed).
Монтирование томов и папок
Последним, но очень важным элементом в нашей Deployment конфигурации будет монтирование томов и папок в контейнеры. Микросервис service-back использует файловую систему контейнера для следущих целей:
— Размещение конфигурационного файла application.yaml
— Размещение логов приложения
Нужно отметить, что application.yaml мы могли бы хранить и внутри образа, но тогда лишились бы возможности менять настройки без пересборки образа и перенаката deployment конфигурации.
Файлы логов можно было бы и не формировать, а писать их только в консоль, откуда они бы централизовано отправлялись kubernetes-ом в лог файлы на поде. Но наше приложение отдельно пишет логи еще и в файле на поде и эти файлы обрабатываются служебным контейнером loging-app. Для чего так сделано вы можете прочесть в статьях про логирование.
Также стандартным вариантом использования файловой системы пода является размещение в ней файлов ключей и сертификатов, которые необходимы для tls-взаимодействия с инфраструктурой — базой данных, брокерами сообщений итд. Но в нашем примере такого нет.
И, наконец, мы выделяем отдельную папку на хосте, куда будут помещаться файлы дампов памяти, потоков итд. Для настройки формирования и записи дампов при падении контейнера используются дополнительные элементы конфигурации. Об этом читайте тут.
Для монтирования томов используется раздел volumes. Причем, тома монтируются для всего пода, а папки для каждого контейнера по отдельности. В нашем примере монтируется 3 разных типа томов:
dump-volume – папка на ноде для помещения в нее дампов вручную и самим приложением
config – том для хранения настроечного файла application.yaml
log – том для записи логов
Для контейнера service-back монтируются следующие файлы и папки:
Папка /dump на ноде для размещения дампов на томе dump-volume
Файл /var/app/config/application.yml в файловой системе контейнера на томе config
Папка /opt/app/log в файловой системе контейнера на томе log
Управление настройками приложения в конфигурациях ConfigMap
Ресурсы ConfigMap используются для хранения пар ключ-значение, причем значение может быть файлом из набора строк. В нашем примере мы храним в ConfigMap единственный ключ application.yaml, а его значением является конфигурационный файл для Spring Boot приложения:
apiVersion: v1
kind: ConfigMap
metadata:
name: service-back-cm-0.01
namespace: myproject
labels:
version: "0.01"
app: service-back
group: service-back
data:
application.yml: |-
logging:
level:
lokrusta.sample: info
org.springframework: info
org.springframework.boot.availability: debug
console:
filter:
level: info
server:
port: 8080
shutdown: graceful
http2:
enabled: true
management:
server:
port: 8081
endpoint:
prometheus:
enabled: true
health:
probes:
enabled: true
endpoints:
web:
exposure:
include: "health,prometheus"
metrics:
export:
prometheus:
enabled: true
app:
metric:
histogramProperties:
percentiles: 0.5, 0.8, 0.95
minExpectedValue: 1
maxExpectedValue: 10000
serviceLevelObjectives: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, 225, 250, 275, 300, 350, 400, 450, 500, 600, 700, 800, 900, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000
timerServiceLevelObjectives: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, 225, 250, 275, 300, 350, 400, 450, 500, 600, 700, 800, 900, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000
workload:
minTimeout: 50
maxTimeout: 150
common:
timeoutMs: 2000
db:
json: true
Далее, в разделе volumeMounts Deployment-конфигурации указано, что значение из поля application.yaml данной ConfigMap надо поместить в файл /var/app/config/application.yml, а этот путь, в свою очередь, указывается в команде запуска нашего Spring Boot приложения.
Такой подход, как уже отмечалось выше, позволяет менять настройки микросервиса, внося изменения только в ConfigMap и далее перезапуская поды service-back (удаляя их в консоли Kubernetes) или перенакатывая Deployment файл для service-back.
Нужно отметить, что многие настройки в application.yaml, а также некоторые значения в самом Deployment-файле необходимо параметризовать в зависимости от среды размещения контейнера. Так на разных средах могут быть разные настройки соединения с БД или, как в нашем примере, вообще, в качестве БД может использоваться json-файл с данными, могут быть разные настройки ресурсов приложения итд
Для параметризации содержимого конфигураций Kubernetes можно использовать движок jinja2, который будет сначала генерировать готовые файлы конфигураций, заменяя в них подстановочные параметры на конкретные значения.
Сами настройки их ConfigMap для service-back мы подробно описывать не будем, так как разработка описание разработки тестового микросервиса выходит за рамки данной статьи, но на некоторых моментах все же остановимся.
Во-первых, отметим, что в application.yaml мы размещаем как встроенные настройки Spring Boot, так и прикладные настройки нашего микросервиса. Прикладные настройки размещены в разделе app.
В настройках для Spring Boot указано:
— Параметры логирования (а сами настройки для logback размещены в logback-spring файле внутри приложения)
— Порт поднимаемого спрингбутом http сервера, включение протокола http2, а также graceful останов сервера (чтобы дать время на завершение выполняющихся запросов)
— В разделе management указан служебный порт для Spring Boot Actuator, а также настройки Spring Boot Actuator
В настройках самого приложения указаны:
— Настройки для гистограмм и перцентилей в метриках. Об этом можно подробно прочитать тут
— Настройка workload для эмуляции задержки ответа от сервиса ( в тестовых целях)
— Настройка timeoutMs, задающая максимальное время выполнения запроса, при превышении которого service-back сам прервет выполнение операции и освободит выполняющийся поток
— Настройка db, в которой указываются параметры подключения к БД, либо как в нашем примере значение json: true, если мы хотим использовать в качестве базы данных сотрудников просто json-файл внутри приложения.
Доступ к микросервису service-back через манифест Service
Наш микросервис service-back предоставляет http-доступ для запросов к Web-серверу на порту 8080 для получения данных о сотрудниках и доступ на порту 8081 для получения метрик и healthcheck-запросов. Чтобы открыть внутри Kubernetes эти точки нужно объявить конфигурацию Service:
apiVersion: v1
kind: Service
metadata:
name: service-back
labels:
app: service-back
service: service-back
namespace: myproject
spec:
ports:
- port: 8080
name: http-8080
- port: 8081
name: http-8081
selector:
app: service-back
Очень важно, что в конце указан selector:
selector:
app: service-back
По этой метке Kubernetes будет понимать к каким нодам применить данную конфигурацию. Чтобы этот манифест был применен к нодам нашего Deployment-файла в нем мы указывали:
selector:
matchLabels:
app: service-back
version: "0.01"
В спецификации сервиса указано, что открыты http порты 8080 и 8081, Запросы приходящие на данный сервис по этим портам будут балансироваться между подами приложения service-back. Таким образом, объект Service обеспечивает еще и балансировку нагрузки, но в данном случае только на уровне подов (не нод!). У каждой конфигурации Service есть тип. Если он явно не указан, то считается что используется тип ClusterIP. Сервисы этого типа не доступны за пределами Kubernetes. Наш микросервис service-back является внутренним и вызывается внешним сервисом service-front, поэтому его нельзя вызвать извне. Но в тестовых целях мы можем открыть доступ к сервису с нашего локального узла командой (где service-back-0.01-6c797fccf6-p6zmz — имя пода):
kubectl port-forward service-back-0.01-6c797fccf6-p6zmz 8080 8081 -n myproject
Далее вызвать сервис, например, через Postman:

Чтобы накатить в Kubernetes созданные нами конфигурации, нужно выполнить команды в следующей последовательности:
kubectl apply -f C:\Work\Projects\SampleOsApp\service-back\os\service-back\kubernetes\service\app_cm.yaml -n myproject
kubectl apply -f C:\Work\Projects\SampleOsApp\service-back\os\service-back\kubernetes\service\app_deployment.yaml -n myproject
kubectl apply -f C:\Work\Projects\SampleOsApp\service-back\os\service-back\kubernetes\service\app_service.yaml -n myproject
Если мы все сделали правильно, Kubernetes создаст в пространстве myproject нужные артефакты и один под сервиса service-back
