![](https://habrastorage.org/getpro/habr/upload_files/986/c3d/9cb/986c3d9cb26fd954f44b60bdee5e43b0.png)
Введение
Если Вы хоть раз работали с высоконагруженными приложениями, то наверняка знаете, какое количество головной боли возникает в процессе. Как правило, в таких приложениях все направлено на максимальную оптимизацию и увеличение быстродействия, но по достижению критической массы пользователей только на оптимизации уже не уехать. Частично эти проблемы помогает решить контейнеризация, которая сегодня есть уже почти у каждого сервиса. При попытке выложить свое приложение во всеобщий доступ, Вам наверняка придется неоднократно столкнуться с этим термином.
Типичные проблемы, с которыми Вы можете столкнуться при расширении аудитории:
Нехватки ресурсов в стрессовых ситуациях.
Усложнение развертывания.
Ухудшается доступность приложения.
Проблемы с расширением инфраструктуры.
Детальнее с Kubernetes
Вы можете познакомиться в его документации. Без него сегодня вряд ли обойдется хоть один большой проект.
В этой статье мы познакомимся с базовым функционалом Kubernetes
и разберемся на практике как можно его применить, развернув тестовое fullstack приложение.
Повествование будет идти от лица Android разработчика, который попал в мир DevOps практически по случайности и пытается разобраться кто есть кто и зачем.
Реализация приложения
Backend
Для реализации серверной части под руку мне попался новенький Ktor, с которым я уже когда-то имел честь познакомиться, но это было весьма скомкано.
На самом деле, выбор фреймворка абсолютно не важен, так как к сути статьи это не имеет значения. В вашем случае, более очевидным выбором может стать Spring.
В моем же случае, шаги по созданию будут следующими:
Генерация проекта на официальном сайте (особенно если у Вас нет Ultimate версии IntelliJ)
При создании нам нужно будет подключить два плагина -
Routing
иKotlinx.Serialization
(опционально)В сгенерированном проекте создадим простейшую модельку пользователя:
@Serializable data class User( val id: Long, val name: String, val age: Int )
В качестве хранилища данных будем использовать мутабельный список, чтобы не усложнять и без того не самый простой процесс.
Добавим эндпоинт для просмотра всех пользователей:
get("/users") { call.respond(HttpStatusCode.OK, users) }
И эндпоинт для создания нового пользователя с минимальной логикой:
post("/users") { val status = runCatching { val user = call.receive<User>() if (user.age >= 18) { users.add(user.copy(id = users.last().id + 1L)) HttpStatusCode.Created } else HttpStatusCode.BadRequest }.getOrDefault(HttpStatusCode.BadRequest) call.respond(status) }
На этом с реализацией серверной части можно закончить. Остальную магию за нас сделает сам Ktor.
Чуть позже мы еще вернемся к этому проекту.
Более подробно с кодом можно ознакомиться на GitHub
Android
В качестве клиентского приложения выберем Android с Compose
. Опять же, в нашем случае, выбор непринципиален.
Этапы создания проекта будут почти что идентичны, поэтому углубляться в реализацию мы не будем:
В Android Studio сгенерируем пустое приложение с
Jetpack Compose
и подключим все необходимые зависимостиС помощью
Retrofit
добавим ранее созданные эндпоинтыinterface Api { @GET("/users") suspend fun getUsers(): Response<List<User>> @POST("/users") suspend fun createUser( @Body user: User ): Response<Unit> }
После этого этапа могут начаться некоторые трудности - с большой вероятностью Ваше Android устройство ничего не знает о
localhost
. И при созданииRetrofit
объекта устройство не поймет, куда ему нужно идти за данными. Пути есть два - поместить сервер и клиент в одну локальную сеть, либо работать с эмулятором. Читать подробнее. В моем случае я выбрал вариант с эмулятором, поэтому мой клиент будет ходить наhttp://10.0.2.2:8080/
. Если Вы хоть раз занимались разработкой Android приложения и backend, то, вероятно, уже знаете как Вам будет проще это решить.Делать запрос на сервер мы, естественно, будем в корутинах и асинхронно отправлять данные в UI:
private fun getUsers() = viewModelScope.launch { val response = api.getUsers() response.takeIf { it.isSuccessful }?.run { _users.value = this.body() ?: emptyList() } ?: run { _error.value = "can't get users: ${response.code()}" } }
Итоговый пользовательский интерфейс выглядит следующим образом:
![](https://habrastorage.org/getpro/habr/upload_files/8fc/eef/a9c/8fceefa9cc321bc5917de9e17271ae99.png)
Подробности реализации можно точно так же найти на GitHub
Развертывание с Kubernetes
И вот наконец мы дошли до самой темы этой статьи.
Перед тем, как приступить к развертыванию, необходимо будет провести дополнительные махинации по настройке всех нужных компонентов:
Включить виртуализацию на компьютере (если она все еще не включена)
Установить WSL (в случае Windows)
Установить Docker Desktop
Установить Minikube
Так как использовать полноценныйKubernetes
будет весьма сложно и затратно, мы выбираем использование Minikube
для создания кластера на локальной машине. Как правило, его используют как раз для разработки и тестирования, поэтому для нас это подходит идеально.
Создание контейнера с сервером
Начать, пожалуй, стоит именно с создания образа для контейнера, потому что без контейнеров - оркестрировать Kuberentes
будет нечем.
Первое, что нужно будет сделать - это сказать докеру то, как и с помощью чего ему нужно создать контейнер. Указываются все шаги в специальном файле - Dockerfile
в корне вашего проекта
FROM openjdk:17-jdk-alpine3.14
RUN mkdir /app
COPY ./build/libs/com.example.ktor-sample-all.jar /app/app.jar
ENTRYPOINT ["java","-jar","/app/app.jar"]
EXPOSE 8080
FROM
указывает базовый образ, к которому в дальнейшем мы будем обращаться. В нашем случае мы запрашиваемjdk
.RUN
выполняет командуmkdir
для создания папки, в которую мы в дальнейшем положим наш файл для исполнения.COPY
, как можно догадаться по названию, скопируетjar
файл из базовой директории в ранее созданную.ENTRYPOINT
выполняет команду с указанными аргументами при запуске контейнера. В нашем случае, запустится исполняемый файл с сервером.EXPOSE
указывает на то, какой рабочий порт у контейнера мы желаем открыть.
Несмотря на маленький размер файла, он может вызвать большое количество головной боли, особенно если у Вас еще нет опыта работы с ним. Как правило, в документации к Вашему фреймворку можно найти подробности по созданию такого Dockerfile.
Однако, создавать образ пока еще рано - мы не подготовили тот самый jar
файл, с которым он будет работать. Сделать это можно довольно просто:
Вызвать сборку руками в консоли -
./gradlew build
или./gradlew assemble
(лично у меня этот вариант работает через раз).Или с помощью вашей IDE, где в
gradle/tasks
можно запуститьassemble
и всё сразу соберётся.Если Вы решили использовать что-то не на
java
илиkotlin
, то опять же эти команды можно найти в документации.
Теперь мы можем со спокойной душой собрать наш образ из корневой директории:docker build -t ktor-sample .
Для проверки можете выполнить команду docker run -P ktor-sample
или docker run -it -p 8080:8080 ktor-sample
.
Поздравляю! Вы создали Ваш первый (а может уже далеко и не первый) Docker
образ. Большинство разработчиков останавливаются примерно на этом моменте, но мы с Вами метим в более серьезную категорию.
Чтобы избежать проблем в будущей работе с Kubernetes
, давайте провернем с Вами еще небольшой трюк, для которого нам понадобится:
Зарегистрироваться на DockerHub.
Теперь можно авторизоваться в него:
docker login -u your login
Попробуем загрузить наш шикарный образ на всеобщее обозрение:
docker push your login/your-container-name
Увы, у нас не получится:
An image does not exist locally with the tag: ...Поэтому сначала добавим тег к нашему образу и повторим шаг 3:
docker tag ktor-sample your login/your-container-name
Делали мы это, так как наш локальный Minikube
знать не знает, что это за образ мы создали и где он находится. Решения может быть два:
Загрузить образ на
DockerHub
(что мы и сделали)
На этом Docker можно оставить в покое.
Работа с Kubernetes
Теперь, прежде чем запускать наш кластер, создадим манифест, в котором объясним Kubernetes
что и как делать с нашим образом.
Объявим Deployment
, который будет поддерживать наше желаемое состояние:
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-deployment
spec:
replicas: 2
selector:
matchLabels:
app: sample-deployment
template:
metadata:
labels:
app: sample-deployment
spec:
containers:
- name: sample-ktor
image: qveex/ktor-sample:latest
ports:
- name: container-port
containerPort: 8080
Из кода можно понять, что наше желаемое состояние - это:
Название
Deployment
, котором будут наши поды.Использовать наш раннее созданный образ
qveex/ktor-sample:latest
.Поддерживать две запущенные реплики этого образа.
Контейнер будет работать по порту 8080.
К сожалению, одного Deployment
для работы будет недостаточно. Дополнительно нам нужно создать Service
, который будет предоставлять нам доступ к ранее объявленным репликам:
apiVersion: v1
kind: Service
metadata:
name: sample-service
spec:
type: LoadBalancer
selector:
app: sample-deployment
ports:
- port: 8080
targetPort: container-port
Здесь все попроще:
Сервис с именем
sample-service
.Тип сервиса будет
LoadBalancer
.А давать доступ сервис будет к порту 8080
А теперь на русском
Базовая единица Kubernetes
- Pod, это обертка над одним контейнером или их группой. Kubernetes
даёт нам возможность создавать Pod'ы, однако даже сама документация не особо рекомендует это делать.
Usually you don't need to create Pods directly, even singleton Pods. Instead, create them using workload resources such as Deployment or Job. If your Pods need to track state, consider the StatefulSet resource.
К использованию рекомендуют как раз Deployment, который мы и использовали ранее, так как он помогает нам, например, перезапускать поды, реплицировать их, распределять ресурсы и т.д.
Поскольку по умолчанию все объекты Kubernetes
крутятся в нем без выходов в реальный мир, мы вынуждены создать Service
, который любезно перенаправит наш сетевой запрос к нужному поду. В нашем примере мы использовал тип сервиса LoadBalancer
, что говорит сервису, чтобы он распределял запросы по разным репликам, а не только в одну. Такой подход помогает оптимизировать нагрузку на наши поды. В нашем синтетическом случае, особый пользы это не принесет, но держать это в голове не помешает.
Упрощенная схема организации выглядит следующим образом:
![Схема организации Схема организации](https://habrastorage.org/getpro/habr/upload_files/b93/cea/bd2/b93ceabd26bda8ed7c707a4e7876c3cc.png)
Как видно, у нас есть две реплики нашего образа, за которыми непрерывно следит Deployment
. Если пользователь хочет до них достучаться, то сначала его запрос попадет в Service
, который решит куда его лучше передать и отдаст дальше в Deployment
. После этого запрос попадет в нужный под, а далее в контейнер внутри пода, и только потом в наш написанный код.
Запутаться в этом крайне легко, но именно эти абстракции и надстройки помогают упрощать и автоматизировать весь процесс развертывания и дальнейшей поддержки.
Продолжаем
Ну и наконец пора поднять наш локальный Kubernetes
кластер с помощью Minikube
.
Для более удобной работы можно сделать
alias kubectl="minikube kubectl --
Запустим:
minikube start
Теперь мы можем воспользоваться нашим манифестом с
Deployment
иService
и создать компоненты нашей схемыkubectl apply -f deployment.yaml
.Чтобы проверить результат, мы можем просмотреть работающие на данный момент поды:
kubectl get pods
Подробнее посмотреть состояние пода можно с помощью команды:kubectl descripe pod pod-name
.
А увидеть его логи можно так:kubectl logs pod-nape
.Если Вы всё сделали правильно, поды должны работать и ожидать запросов к ним. Поэтому следующим шагом мы начнем перенаправлять запрос к ним с помощью сервиса одной из команд:
kubectl port-forward service/sample-service 8080:8080
minikube service sample-service
minikube tunnel
(рекомендует minikube)Теперь можно отправить тестовый запрос из
Postman
или протестировать с помощью нашего клиент приложения. В зависимости от того, как мы прокинули порт, доступ можно получить по:http://localhost:8080/users
(в случаеport-forward
илиtunnel
)http://127.0.0.1:56735/users
(в случаеminikube service
, откроется само)
А с Android эмулятором пойдем по адресуhttp://10.0.2.2:8080/
и получим наш заветныйjson
:[ { "id":1, "name":"Misha", "age":22 }, { "id":2, "name":"Alexandr", "age":25 }, { "id":3, "name":"Rakhim", "age":22 } ]
Поздравляю! Мы только что разработали fullstack приложение и даже умудрились локально развернуть его в Kubernetes
!
Также, можем протестировать работу нашего LoadBalancer
, что он пытается выполнять свою ключевую обязанность - распределение нагрузки.
Быстро начнем отправлять запросы с разных клиентов (я пробовал с помощью браузера и postman). Если заглянуть в логи подов, то можно увидеть следующую картину:
Под sample-deployment-7845fc9b8d-ptpkr
:
2023-12-14 15:22:47.702 [eventLoopGroupProxy-4-1] TRACE io.ktor.routing.Routing - Trace for [users]
/, segment:0 -> SUCCESS @ /
/users, segment:1 -> SUCCESS @ /users
/users/(method:GET), segment:1 -> SUCCESS @ /users/(method:GET)
/users/(method:POST), segment:1 -> FAILURE "Selector didn't match" @ /users/(method:POST)
Matched routes:
"" -> "users" -> "(method:GET)"
Route resolve result:
SUCCESS @ /users/(method:GET)
Под sample-deployment-7845fc9b8d-bz4vq
:
2023-12-14 15:22:45.856 [eventLoopGroupProxy-4-1] TRACE io.ktor.routing.Routing - Trace for [users]
/, segment:0 -> SUCCESS @ /
/users, segment:1 -> SUCCESS @ /users
/users/(method:GET), segment:1 -> SUCCESS @ /users/(method:GET)
/users/(method:POST), segment:1 -> FAILURE "Selector didn't match" @ /users/(method:POST)
Matched routes:
"" -> "users" -> "(method:GET)"
Route resolve result:
SUCCESS @ /users/(method:GET)
Как можно заметить, запросы действительно, приходят в разные реплики нашего приложения! И всё благодаря LoadBalancer
.
А из-за того что мы использовали в качестве хранилища список, то при создании пользователей еще можно заметить несогласованность получаемых данных, так как пользователь создаётся только в одном из подов. Но это уже тема для другой статьи...
Заключение
В результате статьи, мы имеем полноценно работающий проект, который станет отличной отправной точкой в мире DevOps. Такое количество абстракций, виртуализация и т.п., что мы с Вами сделали, помогает максимально отвязаться от реализации наших проектов, что заметно может упростить работу при развёртке и поддержке.
![](https://habrastorage.org/getpro/habr/upload_files/5d1/eac/6ec/5d1eac6ece982f5b5bc3e97d9362b350.jpg)
Если Вы дочитали до этого момента, то теперь можете смело добавить навыки DevOps в своё резюме начинать более глубокое изучение Kubernetes
на более сложных примерах с участием других компонентов (например, ConfigMap, Secrets, Ingress и т.д.) и детальнее изучить их строение изнутри.
И если Вы тоже только разбираетесь в этой области, как и я, то надеюсь, что это статья была для Вас хоть капельку полезна. ?