--- title: Начало работы с kubernetes description: Как там это всё разворачивать в двух словах. category: DevOps position: 4 --- # Проблема в изучении кубернетес Многие люди, кто задавались вопросом "Как начать работать с кубернетес?", сталкивались с тем, что документация крайне большая сложная и нет нормального описания как завернуть маленький проект из одного контейнра в свой кластер или как развернуть сам кластер без боли. А всё потому что вся документация нацелена на большие production-ready системы с большим rps и тому подобным.
В данной статье я попробую исправить это вселенское недопонимание используя k3s, свой комплюктер и немного знаний по кодингу.
![Intro image](/images/k3s_start/kube_intro.jpg)
# Что такое кубернетес и почему это лучше докера Многие ребята, кто хорошо знаком с докером и его возможностями, могут задаваться таким вопросом. Для тех кто в танке, напомню, что докер имеет вариант запуска в режиме кластера. Этот функционал называется [docker swarm](https://docs.docker.com/engine/swarm/). В целом, swarm отдалённо напоминает kubernetes, так как в этом режиме докер худо-бедно умеет автоскейлится и запускаться в кластере, но это всё равно немного не то. Также замечу, что кубер активно развивается и поддерживается. огромным количество компаний. А вот docker swarm уже по-немногу умирает и документация по нему не то чтобы супер хороша. По большей части кубернетес это система, которая будет управлять вашими приложениями, следить за их состоянием и помогать вам в их конфигурации. Кубер умеет очень много, поэтому все интересные способности я в этой статье не смогу осветить, но самую базу попробую рассказать. # Из чего состоит кубернетес Так как я в этой статье хотел затронуть совсем базовые и практические вещи, то рассматривать мы будем только самые часто используемые компоненты. - Container - Pod - Deployment - Service - Ingress - Namespace - Secret - ConfigMap А теперь рассмотрим каждый ресурс немного поподробнее. ## Container Контейнеры не являются чем-то специфичным для кубернетес. С контейнерами вы можете быть знакомы из кучи других систем. В контексте кубера они не обладают никакими дополнительными свойствами. Это ровно то же, что и контейнеры `containerd` или те, с которыми вы возились с докером. Ничего нового. Собираются контейнеры для кубернетеса ровно тем же образом, что и для докера. Важная ремарка. Кубер, начиная с 2021 года, не поддерживает докер как бэкенд. Теперь он будет общаться с containerd напрямую. Это значит, что перед использованием контейнеров собранных на локальной машине надо будет импортировать их в `containerd` используя `ctr image import`. Как импортировать образы почитать можно в [этой статье](https://cwienczek.com/2020/06/import-images-to-k3s-without-docker-registry/). ## Pod
Pods
Поды - это логически связанные группы контейнеров. Это самая базовая еденица кубернетеса. В поде находится от одного до множества контейнеров. Интересная особенность пода в том, что все контейнеры делят один сетевой адрес. Другими словами, если у вас один из контейнеров открыл порт `3000`, то другие контейнеры из пода этот порт использовать не смогут. То есть, если вы хотите логически связанные приложения поместить в под, то они могут ходить друг к другу через лупбек адреса. Такие как `localhost` или `127.0.0.1`. ## Deployment Поды это круто, но есть одно но. Данные объекты неизменяемые и сам под скейлить вручную занятие сомнительное. Конечно, никто не мешает создать вам вручную [ReplicaSet](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/) и самому крутить там нужные значения. Но в среднем вам с этим возиться не очень то хочется.
Deployment нужен именно для описания подов и создания некоторых ресурсов, нужных для скейлинга. Также с помощью деплойментов можно делать откаты приложения через механиз роллбеков. Я это рассматривать не буду. В этой статье только база. ## Service Сервис - это ресурс, с помощью которого поды могут общаться между собой. По факту сервис описывает, какие порты пода открыты и перенаправляет трафик на них. Представьте себе под, который просто проксирует весь трафик с одного порта на какой-нибудь порт пода. Это и есть Service.
![Service overview](/images/k3s_start/service%20overview.png)
Выглядит сервис примерно так как показано на кртинке выше. Он направляет весь входной трафик с указанного порта на порты подов. Также сервис выступает как балансировщик. Если вы хотите сделать запрос от одного пода до дргого, внутри кластера, то вам придётся использовать сервис.
Если вы попробуете сделать запрос напрямую по IP пода, то у вас, конечно же, получится, но это довольно странная идея из-за того, что при пересоздании пода у него может обновится IP внутри кластера. Поэтому лучше использовать сервисы. Также они потребуются для ингресса.
## Ingress Ингресс - это сервис описывающий куда пускать трафик, который поступает снаружи кластера.
![Ingress overview](/images/k3s_start/ingress-overview.png)
Принцип работы ингресса следующий: Вы указываете хост ингресса и различные пути. В зависимости от входящего запроса ингресс выбирает в какой сервис направить запрос и в какой порт. Настройка ингресса достаточно гибкая и я не думаю, что вы можете столкнуться с какими либо проблемами. ## Namespace Неймспейсы это логические разделители уровня доступа. Я предпочитаю использовать разные неймспейсы под различные логические группы приложений. Например, в моём кластере есть отдельные неймспейсы для каждого приложения. Скажем, у меня есть проект, в котором есть база данных и веб сервер. Я держу их в одном неймспейсе, который называется также, как и приложени. А вот телеграм боты -- приложения достаточно лёгкие. Поэтому у меня есть неймспейс со всеми телеграм ботами. Эта штука позволяет просто лучше организовать свой кубернетес кластер. Также неймспейсы очень полезны для ограничивания возможностей конкретного пользователя. Например, вы можете создать правило, которое будет разрешать пользователю смотреть на поды в каком-то неймспейсе, но при этом ему нельзя будет что либо изменять. ## Secret и ConfigMap Данные ресурсы нужны только чтобы хранить внутри кластера какую-нибудь информацию. Например вы можете сохранить в ConfigMap переменные среды и потом использовать их в подах в одном неймспейсе. В секреты обычно кидают сертификаты или какие-нибудь ключи. Но так как секреты не особо секретные принято использовать [Vault](https://www.vaultproject.io/docs/platform/k8s). Но так как эта статья затрагивает только основы рассматривать развертку и настройку Vault мы не будем, ну и также HashiCorp всё довольно подробно расписали сами. # Как развернуть k8s у себя Для локального кубера есть пара вариантов. - k3s (Недоступен под Windows) - minikube На первый взгляд minikube может показаться лучшим вариантом. И он действительно хорош тем, что его легко развернуть и почистить после своих экспериментов. Однако, там есть проблемы с ингрессами. По факту они не работают и там надо окольными путями получать адреса.
k3s - это легковесная production-ready реализация k8s. Ingress у него работают отлично, поэтому я буду использовать его.
Я не буду зацикливаться на установке `minikube`, так как он прост в установке и первоначальной настройке. Почитать подробнее можно в [официальном гайде от minikube](https://minikube.sigs.k8s.io/docs/start/). С `k3s` всё немного посложнее, но тоже достаточно просто, если немного разобраться. Первоначальная установка описана в [официальной доке k3s](https://rancher.com/docs/k3s/latest/en/installation/install-options/). ## Подключение кластера После установки `kubectl` в домашней дериктории должен был быть сгенерирован файл `.kube/config`. Этот файл содержит данные для подключения к различным кластерам. `minikube` Сам добавляет ключи для подключения к .kube/config. `K3S` никак не изменяет `.kube/config`, поэтому надо будет это сделать вручную. Для того, чтобы это сделать сначала разберёмся как выглядит конфиг. ```yaml{}[.kube/config] apiVersion: v1 kind: Config preferences: {} # Массив кластеров. # Каждый элемент -- данные для подключения # Тут также есть названия для каждого кластера. clusters: - name: hyper cluster: certificate-authority-data: DATA+OMITTED server: https://192.168.1.55:6443 - name: k3s-local cluster: certificate-authority-data: DATA+OMITTED server: https://127.0.0.1:6443 # Массив данных пользователя. # Тут указаны пользователи с # различными сертификатами. # Обычно для разных серверов у вас будут # Различные данные для входа. users: - name: hyper-s3rius user: client-certificate-data: REDACTED client-key-data: REDACTED - name: k3s-user user: client-certificate-data: REDACTED client-key-data: REDACTED # Массив контекстов. # Контекст - это связующее звено # между кластероами и пользователями. contexts: - context: cluster: hyper user: hyper-s3rius name: hyper - context: cluster: k3s-local user: k3s-user name: k3s # Текущий контекст указывает какой # контекст использовать по умолчанию. current-context: "k3s" ``` Для работы с кубером из командной строки можно использовать `kubectl`. Чтобы сменить контекст в любой команде вы можете передать параметр `--context $CONTEXT_NAME`, где `$CONTEXT_NAME` это название контекста. Чтобы достать данные для подключения к `k3s` надо посмотреть его конфиг и скопировать данные. Либо выставить переменную среды `KUBECONFIG`, которая будет указывать конфиг k3s. Конфиг подключения `k3s` лежит в файле `/etc/rancher/k3s/k3s.yaml`. Можете выполнить команду, которая будет просить kubectl использовать указанный конфиг: `export KUBECONFIG=/etc/rancher/k3s/k3s.yaml`
Либо скопируйте нужные данные для подключения себе в `.kube/config`. После настройки выполните команду и проверьте что вам вернулось что-то подобное. ```bash $ kubectl --context "my-context" get pods No resources found in default namespace. ``` Это значит, что никаких ресурсов пока в кластере нет. Это мы исправим чуть позже. Пока что можно сказать, что подключение прошло успешно. Если же у вас выпадает ошибка, например такая: ``` The connection to the server localhost:8080 was refused - did you specify the right host or port? ``` То либо запустите кластер ``` sudo systemctl start k3s.service ``` Либо у вас неверные данные для входа. Перепроверьте свой `.kube/config`. ### Мониторинг кластера Для просмотра ресурсов и управления кластером используется `kubectl`. Но для того, чтобы с ней работать нужно получше понять, что вообще в кластере есть. Для этого я советую использовать [Lens](https://k8slens.dev/). Это крайне удобный интерфейс для управления своим кластером. Также там есть очень клёвая настройка, которая сама включит мониторинг потребления памяти и процессора для всех подов и кластера в общем. На локальной машине это не имеет смысла, а вот в проде было бы очень полезно. Выглядит Lens примерно так:
![Lens example](/images/k3s_start/lens-example.png)
Для изучения **крайне настоятельно рекомендую** настроить Lens. # Ваше первое приложение в кластере. Вы можете следовать статье, а можете подсмотреть весь код в [репозитории](https://github.com/s3rius/blog_examples/tree/master/req_counter). ### Сервер Давайте создадим своё первое приложение. Для этого я буду использовать [express.js](https://expressjs.com/), так как он крайне популярен и прост. Для этого сначала напишем сам сервер. Я буду использовать yarn, но можете и npm, сути не поменяет. Создайте какую-нибудь папку, где вы будете эксперементировать, откройте в ней ваш любимый тектовый редактор и просто копируйте файлы ниже. ```json{}[package.json] { "name": "req_counter", "version": "1.0.0", // Указан модуль, // чтобы использовать нормальны импорты, // а не require. "type": "module", "license": "MIT", "scripts": { // Скрипт старта сервера. "server": "node index.js" }, // Зависимости проекта. "dependencies": { "express": "^4.17.1" } } ``` И само приложение ```js{}[index.js] import express from "express"; import { hostname } from "os"; import { argv, exit } from "process"; // Серверное приложение. const app = express(); // Глобальный счётчик входящих запросов. let requests = 0; // Обработка входящего запроса. app.get("*", (req, res) => { // Увеличиваем глобальный счётчик запросов. requests += 1; // Логгируем входящий запрос. console.log(`${req.method} ${req.url}`); // Возвращаем информацию о текущем хосте и количестве запросов. res.json({ requests: requests, hostname: hostname(), }); }); // Аргументы командной строки. // Напрмиер yarn run server 127.0.0.1 8000 const args = argv.slice(2); // Если передано неверное количество аргументов. if (args.length != 2) { console.error("Usage: yarn run server {host} {port}"); exit(1); } // Простейший "парсинг" аргументов командной строки. const host = args[0]; const port = args[1]; // Старт сервера. app.listen(port, host, () => { console.log(`Server listening at http://${host}:${port}`); }); ``` Это всё. Сервер готов.
Протестируем запуск сервера, выполнив команду ниже и открыв в своём любимом браузере http://localhost:8080. ``` yarn run server 0.0.0.0 8080 ``` У меня всё работает и успешно отдаётся нужная информация. ```json { "requests": 1, "hostname": "s3rius-pc" } ``` ### Docker образ Теперь создадим докер образ приложения. Добавим `.dockerignore`, чтобы игнорировать ненужные файлы во время сборки образа. ```gitignore{}[.dockerignore] node_modules/ ``` И добавим в проект `Dockerfile` для описания самого процесса сборки. ```dockerfile{}[Dockerfile] FROM node:17-alpine WORKDIR /app COPY . /app/ RUN yarn install CMD [ "yarn", "run", "server", "0.0.0.0", "8000"] ``` Давайте соберём и запустим проект в контейнере. ```bash docker build --tag="s3rius/req-counter-express:latest" . docker run --rm -it -p 3400:8000 "s3rius/req-counter-express:latest" ``` Можете проверить, что приложение работает успешно, открыв в браузере http://localhost:3400. У меня в ответ пришло то же сообщение. Только, как можно заметить, `hostname` поменялся. На самом деле контейнеры используют свои hostname отличные от `hostname` локальной машины. ```json { "requests": 10, "hostname": "8f23adadc640" } ``` Вариантов как положить это приложение в K8S несколько. - Вы можете запушить собранное приложение в [Docker HUB](https://hub.docker.com/) и использовать его. - Можете использовать мой образ `s3rius/req-counter-express:latest` - Сохранить собранный образ как tar файл и импортировать его в containerd напрямую. Как это сделать почитать можно в [этой статье](https://cwienczek.com/2020/06/import-images-to-k3s-without-docker-registry/). ### Деплой в k8s Создайте папку `kube` в папке проекта и теперь мы будем работать в ней. Все ресурсы будут описаны yaml-файлами. ```yaml{}[kube/deployment.yml] --- apiVersion: apps/v1 kind: Deployment # метаданные самого деплоймента. metadata: name: req-counter-deployment spec: # Количество реплик пода. replicas: 1 # Селектор, который выбирает # какие поды принадлежат этому деплойменту. selector: matchLabels: app: req-counter # Шаблон пода, # который будет использоваться при # маштабировании. template: # Метаданные пода. # тут обычно помещаются лейблы, # с помощью которых деплоймент идентифицирует # свои поды. metadata: labels: app: req-counter spec: # Масссив контейнеров containers: # Название контейнера внутри пода. - name: req-counter-app # Образ приложения. image: s3rius/req-counter-express:latest # Ресурсы требуемые для работы приложения. resources: # Минимальное количество ресурсов, # которое кластер гарантированно предоставит приложению. # Также данные значения используются для того, # чтобы выяснить на какой ноде запускать приложение. requests: memory: "50Mi" cpu: "30m" # Максимально возможные значения приложения. # Если приложение выйдет за лимиты, # то кубер убьёт приложение. limits: memory: "128Mi" cpu: "100m" # Порты на которых открыты приложения. ports: - containerPort: 8000 protocol: TCP ``` Теперь опишем сервис для управления трафиком. ```yaml{}[kube/service.yml] --- apiVersion: v1 kind: Service # Метадата сервиса. metadata: name: req-counter-service spec: # Селектор подов, # которым будет пускаться трафик. # Трафик может идти в любой под, # который матчит данному селектору. selector: app: req-counter # Порты для проксирования соединений. ports: # Порт сервиса - port: 80 # Порт пода, куда будет идти трафик дальше. targetPort: 8000 ``` И в последнюю очередь опишем наш ингрес. ```yaml{}[kube/ingress.yml] --- apiVersion: networking.k8s.io/v1 kind: Ingress # Метаданные ингресса. metadata: name: req-counter-ingress labels: name: req-counter-ingress spec: # Правила роутинга. rules: # Требуемый хост. - host: req-counter.local http: paths: # Тип пути Prefix значит, # что все запросы, которые начинаются c # ${path} будут матчится тут. - pathType: Prefix # Сам путь. path: "/" backend: service: # Название нашего сервиса. name: req-counter-service # Порт сервиса, куда перенаправлять входящий трафик. port: number: 80 ``` Перед тем, как создать все описанные ресурсы создадим неймспейс. ```bash $ kubectl --context k3s create namespace req-couter-ns namespace/req-couter-ns created ``` После того, как неймспейс создан самое время сделать последний штрих.
![Apply meme](/images/k3s_start/kubectl-apply.png)
```bash $ kubectl --context k3s --namespace req-couter-ns apply -f ./kube/ deployment.apps/req-counter-deployment created ingress.networking.k8s.io/req-counter-ingress created service/req-counter-service created ``` Готово. Теперь вы можете зайти в lens, выбрать свой кластер из списка и посмотреть как там поживает ваше приложение. Также не забудьте указать неймспейс, в который вы деплоили приложение. Выглядит это чудо примерно так:
![Deployed app in lens](/images/k3s_start/lens-deployed.png)
Также можно использовать команду и вывести всё себе в терминал. ``` $ kubectl --context k3s --namespace req-couter-ns get all NAME READY STATUS RESTARTS AGE pod/req-counter-deployment-764476db97-dt2tc 1/1 Running 0 8m11s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/req-counter-service ClusterIP 10.43.50.23 80/TCP 8m11s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/req-counter-deployment 1/1 1 1 8m11s NAME DESIRED CURRENT READY AGE replicaset.apps/req-counter-deployment-764476db97 1 1 1 8m11s ``` ### Маштабирование В рамках демонстрации давате поменяем значение `replicas` в нашем файле деплоймента до `3`. ```yaml{}[kube/deployment.yml] ... spec: # Количество реплик пода. replicas: 3 ... ``` После изменения просто ещё раз вызовите команду apply. ```bash $ kubectl --context k3s --namespace req-couter-ns apply -f ./kube/ deployment.apps/req-counter-deployment configured ingress.networking.k8s.io/req-counter-ingress unchanged service/req-counter-service unchanged ``` Как можно видеть, изменился только наш `Deployment`. Остальные ресурсы остались нетронутыми. Давайте посмотрим на поды в нашем неймспейсе. ``` $ kubectl --context k3s --namespace req-couter-ns get pods NAME READY STATUS RESTARTS AGE req-counter-deployment-764476db97-dt2tc 1/1 Running 0 13m req-counter-deployment-764476db97-tdjrb 1/1 Running 0 69s req-counter-deployment-764476db97-x28fr 1/1 Running 0 69s ``` Как видно, всё правильно. Теперь у нас 3 пода нашего приложения. Теперь можно выполнить кучу запросов по адресу http://req-counter.local/ и получить балансировку между подами из коробки, без дополнительных конфигураций. Если у вас не получается найти адрес. Добавьте данный хост себе в `/etc/hosts` на линуксе или в `C:\Windows\System32\drivers\etc\hosts` на windows, дописав в конец файла следующее: ```[/etc/hosts] 127.0.0.1 req-couter.local ``` И теперь вы можете открыть в браузере своё приложение и увидеть, как кубернетес заботливо направляет трафик туда, куда вы хотите.
![Making requests](/images/k3s_start/requests.gif)
### Как мне очистить мой кластер? Всё очень просто. Так как у нас имеются описния ресурсов, то мы можем удалить всё сразу используя команду ```bash $ kubectl --context k3s --namespace req-couter-ns delete -f ./kube/ deployment.apps "req-counter-deployment" deleted ingress.networking.k8s.io "req-counter-ingress" deleted service "req-counter-service" deleted ``` Таким образом k8s удалит все описанные ресурсы из вашего кластера. До новых встреч.