О Kubernetes и его роли в построении микросервисных приложений
известно, пожалуй, большинству современных IT-компаний. Однако при
его внедрении часто возникает вопрос какой вариант установки
выбрать: Self-Hosted или Managed-решение от одного из облачных
провайдеров. О недостатках первого варианта, думаю, известно всем,
кто проходил через ручное конфигурирование K8s: сложно и трудоемко.
Но в чем лучше Cloud-Native подход?
Я Василий Озеров, основатель агентства Fevlake и действующий
DevOps-инженер (опыт в DevOps 8 лет), покажу развертывание Kubernetes-кластера
на базе облака Mail.ru Cloud Solutions. В этом цикле статей мы
создадим MVP для реального приложения, выполняющего транскрибацию
видеофайлов из YouTube.
На его базе мы посмотрим все этапы разработки Cloud-Native
приложений на K8s, включая проектирование, кодирование, создание и
автомасштабирование кластера, подключение базы данных и S3-бакетов,
построение CI/CD и даже разработку собственного Helm-чарта.
Надеюсь, этот опыт позволит вам убедиться, что работа с K8s может
быть по-настоящему удобной и быстрой.
В первой части статьи мы выберем архитектуру приложения, напишем
API-сервер, запустим Kubernetes c балансировщиком и облачными
базами, развернем кластер RabbitMQ через Helm в Kubernetes.
Также записи всех частей практикума можно посмотреть:
часть 1,
часть 2,
часть 3.
Выбор архитектуры приложения
Определимся с архитектурой будущего приложения. В первую очередь
нам потребуется API, к которому будет обращаться клиентское
приложение. Будем использовать стандартные форматы: HTTPS и JSON. В
JSON необходимо передавать URL видео, а также некоторый
идентификатор или уникальное имя запроса для возможности
отслеживания его статуса.
Следующий необходимый компонент очередь сообщений. Очевидно, что
обработку видео не получится проводить в real-time режиме. Поэтому
будем использовать RabbitMQ для асинхронной обработки.
Далее нам потребуются обработчики, которые будут читать
сообщения из очереди и заниматься непосредственной конвертацией
запрошенных видео в текст. Назовем их Worker. Для транскрибации
будем использовать не внешнее API, а какую-нибудь библиотеку,
установленную локально. Так как для этого потребуются ресурсы,
обязательно настроим автомасштабирование в кластере, чтобы число
обработчиков изменялось пропорционально количеству сообщений в
очереди.
Для сохранения текстовых расшифровок видео, которые будут
формировать обработчики Worker, потребуется хранилище. Будем
использовать
S3, которое идеально подходит для хранения неструктурированных
данных в облаке.
Наконец, чтобы иметь возможность получать статус обработки
запросов, их необходимо где-то сохранять. Для этого выберем обычную
базу PostgreSQL.
Сценарий взаимодействия выбранных компонентов включает в себя
следующие шаги:
-
Клиент отправляет на API-сервер запрос POST, передавая в теле
запроса имя и URL видео на YouTube, которое необходимо перевести в
текст.
-
API-сервер формирует сообщение с полученными параметрами и
передает его в очередь RabbitMQ.
-
API-сервер сохраняет информацию о полученном запросе на
конвертацию видео в базе данных PostgreSQL. Статус обработки
запроса по умолчанию равен false.
-
API-сервер информирует клиента об успешном завершении операции.
Клиент может продолжать свою работу, не дожидаясь конвертации
видео.
-
Свободный обработчик Worker извлекает сообщение из очереди
RabbitMQ.
-
Получив сообщение, Worker выполняет его обработку: загружает
видео по указанному URL, получает из него аудио и переводит при
помощи стороннего ПО в текст.
-
Обработав видео, Worker сохраняет транскрипт видео в хранилище
S3.
-
Worker отправляет в API-сервер информацию об успешной обработке
запроса с исходным именем. В запросе передается статус обработки,
равный true, и ссылка на текстовый файл в S3. Endpoint для отправки
статуса обработки запросов можно либо жестко прописывать в
environment-переменных обработчика Worker, либо передавать его в
теле сообщений наряду с другими параметрами. В нашем MVP будет
реализован первый вариант. То есть обработчикам будет известно,
какой API вызвать для обновления статуса запросов.
-
API-сервер обновляет полученную от Worker информацию о запросе в
базе данных PostgreSQL. Альтернативный вариант можно настроить
обновление базы данных непосредственно из обработчиков Worker,
однако это потребует знания структуры БД с их стороны, что чревато
проблемами при миграциях БД. Поэтому в нашем приложении
взаимодействие с БД будет происходить исключительно через
API-сервер.
-
Клиент спустя некоторое время после отправки исходного видео
запрашивает статус его обработки, передавая в API-сервер имя
исходного запроса.
-
API-сервер извлекает данные о запросе из PostgreSQL по
полученному имени.
-
API-сервер получает информацию о запросе из PostgreSQL.
-
API-сервер отправляет данные о запросе клиенту. Клиент получает
статус обработки и URL, по которому сможет в дальнейшем загрузить
транскрипт исходного видео из S3.

Упрощенная схема архитектуры будущего приложения
Настройка кластера Kubernetes в облаке MCS
Начинаем с создания кластера Kubernetes. Для этого в панели
управления облаком MCS необходимо выбрать пункт меню
Контейнеры Кластеры Kubernetes и добавить новый кластер.
На первом шаге настраивается конфигурация будущего кластера.
Можно выбрать тип среды и один или несколько предустановленных
сервисов. Мы выберем среду Dev и сразу добавим Ingress Controller
Nginx для управления внешним доступом к кластеру:
На следующем шаге вводим название кластера и выбираем тип
виртуальной машины для ноды Master. Оставим стандартную
конфигурацию с 2 CPU и 4 ГБ памяти. Далее можно указать зону
доступности мы оставим для нее автоматическое заполнение:
Далее на этом же шаге выбирается тип и размер диска. Нам
достаточно HDD размером 20 Гб. Оставляем одну Master-ноду, выбираем
предварительно добавленную подсеть и назначаем внешний IP для
удобного доступа к кластеру извне:
На следующем шаге создаются группы рабочих узлов. В рамках
проекта нам потребуются две группы. Сейчас создадим первую для
развертывания API и RabbitMQ, а впоследствии добавим еще одну, для
обработчиков Worker.
Вводим название группы узлов и указываем конфигурацию: 2 CPU и
4ГБ памяти. Для зоны доступности вновь выбираем автоматический
выбор:
Чтобы обеспечить работу RabbitMQ, выбираем более
производительный тип дисков SSD размером 50 ГБ. Оставляем один
узел, автомасштабирование пока не указываем его рассмотрим позднее
на примере другой группы узлов:
На последнем шаге запускается процесс формирования кластера,
который может занять некоторое время: от 5 до 20 минут.
При успешном добавлении кластера на экране отобразится
информация о его параметрах:
Для последующей работы с кластером необходимо:
-
Установить локальный клиент kubectl и запустить его.
-
Экспортировать в локальный клиент конфигурационный файл
созданного кластера с расширением .yaml командой export
KUBECONFIG=<путь к файлу>.
-
Для безопасного подключения к кластеру запустить proxy-сервер
командой kubectl proxy.
Эта инструкция отображается под списком параметров кластера
после его добавления.
У нас kubectl установлен поэтому берем из загрузок
сформированный конфигурационный файл kub-vc-dev_kubeconfig.yaml и
экспортируем его в kubectl:
После экспорта конфигурационного файла можно убедиться в
работоспособности кластера:
-
Сначала смотрим доступные контексты: kubectl config
get-contexts
Видим, что у нас создался кластер kub-vc-dev:

-
Смотрим доступные ноды: kubectl get nodes
В кластере создались две ноды master и workload:

-
Смотрим доступные Namespace: kubectl get ns
Получаем ответ:

-
Смотрим доступные поды: kubectl -n ingress-nginx get
pods
В Namespace ingress-nginx запущены поды для Nginx
Controller:

-
Смотрим доступные сервисы: kubectl -n ingress-nginx get
svс
В списке сервисов также отображается Nginx Controller, для
которого указан внешний адрес, который мы сможем прописывать в DNS,
чтобы попадать в наши сервисы извне:
Разработка API-сервера на Go
Следующий шаг написать API для отправки запросов на конвертацию
видео и получения статуса их обработки. С полной версией исходного
кода можно
ознакомиться здесь.
Ниже отображена структура проекта. Это стандартное
Go-приложение. В файлах go.mod, go.sum описываются зависимости, в
папке migrations миграции для базы данных PostgreSQL. В main.go
содержится основная логика программы, в requests.go реализация API
на добавление, редактирование, удаление и выборку запросов. И есть
Dockerfile.

Структура API-сервера
Остановимся подробнее на содержимом main.go.
Вначале импортируем нужные зависимости. В первую очередь, это
migrate
для автоматического осуществления миграций, database/sql для работы
с базами данных, go-env
для работы с переменными окружения, web-фреймворк Gorilla
и AMQP
для работы с RabbitMQ:
package mainimport ( "encoding/json" "os" "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file" "database/sql" env "github.com/Netflix/go-env" _ "github.com/lib/pq" "log" "net/http" "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/streadway/amqp")
Далее идут environment, которые мы будем использовать. PGSQL_URI
и RABBIT_URI нужны для того, чтобы подключиться к PostgreSQL и
RabbitMQ соответственно, LISTEN номер порта, на котором необходимо
слушать входящие запросы:
type environment struct { PgsqlURI string `env:"PGSQL_URI"` Listen string `env:"LISTEN"` RabbitURI string `env:"RABBIT_URI"`}
Далее следует функция main, которая занимается инициализацией.
Сначала происходит чтение environment-переменных, подключение к
базе данных PostgreSQL и запуск миграций:
func main() {var err error// Getting configurationlog.Printf("INFO: Getting environment variables\n")cnf := environment{}_, err = env.UnmarshalFromEnviron(&cnf)if err != nil { log.Fatal(err)}// Connecting to databaselog.Printf("INFO: Connecting to database")db, err = sql.Open("postgres", cnf.PgsqlURI)if err != nil { log.Fatalf("Can't connect to postgresql: %v", err)}// Running migrationsdriver, err := postgres.WithInstance(db, &postgres.Config{})if err != nil { log.Fatalf("Can't get postgres driver: %v", err)}m, err := migrate.NewWithDatabaseInstance("file://./migrations", "postgres", driver)if err != nil { log.Fatalf("Can't get migration object: %v", err)}m.Up()
Затем следует подключение к RabbitMQ и инициализация работы с
ним:
// Initialising rabbit mq// Initing rabbitmqconn, err := amqp.Dial(cnf.RabbitURI)if err != nil { log.Fatalf("Can't connect to rabbitmq")}defer conn.Close()ch, err = conn.Channel()if err != nil { log.Fatalf("Can't open channel")}defer ch.Close()err = initRabbit()if err != nil { log.Fatalf("Can't create rabbitmq queues: %s\n", err)}
И в завершение запускается web-сервер. При этом каждому из
возможных API-запросов сопоставляется функция обработки, описанная
в отдельном файле requests.go:
// Setting handlers for querylog.Printf("INFO: Starting listening on %s\n", cnf.Listen)router := mux.NewRouter().StrictSlash(true)// PROJECTSrouter.HandleFunc("/requests", authMiddleware(getRequests)).Methods("GET")router.HandleFunc("/requests", authMiddleware(addRequest)).Methods("POST")router.HandleFunc("/requests/{name}", authMiddleware(getRequest)).Methods("GET")router.HandleFunc("/requests/{name}", authMiddleware(updRequest)).Methods("PUT")router.HandleFunc("/requests/{name}", authMiddleware(delRequest)).Methods("DELETE")http.ListenAndServe(cnf.Listen, handlers.LoggingHandler(os.Stdout, router))
Далее следует аутентификация в сильно упрощенном варианте, так
как на стадии MVP этого достаточно. Разумеется, при разработке
Enterprise-решений указание токенов и прочих переменных в явном
виде неприемлемо:
func authMiddleware(next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tokenString := r.Header.Get("X-API-KEY") if tokenString != "804b95f13b714ee9912b19861faf3d25" { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("Missing Authorization Header\n")) return } next(w, r) })}
Переходим к инициализации RabbitMQ. Тут мы будем использовать
два Exchange и три очереди.
Первый Exchange VideoParserExchange. К нему подключены две
очереди:
-
VideoParserWorkerQueue это основная очередь, которую будут
слушать обработчики (на иллюстрации для примера приведен один
обработчик Worker-0).
-
VideoParserArchiveQueue архивная очередь, в которую дублируются
сообщения на случай возникновения ошибок. Вместо нее можно
использовать другие средства бэкапирования, например хранилище
S3.
У VideoParserExchange тип fanout, это значит, что все сообщения
из него будут отправляться во все подключенные очереди
одновременно.
Второй Exchange VideoParserRetryExchange, к нему подключена
очередь VideoParserWorkerRetryQueue. К ней не подключены
обработчики.

Архитектура очередей сообщений
Цель такого решения отложить попытки отправки сообщений на
вышедшие из строя Worker до момента, когда они с большей долей
вероятности смогут вернуться к обработке.
Например, если во время обработки сообщения из основной очереди
обработчик по какой-то причине отключится и не обработает
сообщение, то оно отправится в VideoParserRetryExchange. Этот
переход настроен при помощи параметра x-dead-letter-exchange.
Далее VideoParserRetryExchange отправит сообщение в очередь
VideoParserWorkerRetryQueue. В ней при помощи параметра
x-message-ttl ограничено время хранения сообщения. Также при помощи
параметра x-dead-letter-exchange мы указываем, что по прошествии
таймаута сообщение должно вернуться в VideoParserExchange для
последующей обработки.

Алгоритм работы очередей сообщений
Вся эта логика описана в функции initRabbit. Сначала мы
объявляем два Exchange:
func initRabbit() error { err := ch.ExchangeDeclare( "VideoParserExchange", // name "fanout", // type true, // durable false, // auto delete false, // internal false, // no wait nil, // arguments ) if err != nil { return err } err = ch.ExchangeDeclare( "VideoParserRetryExchange", // name "fanout", // type true, // durable false, // auto delete false, // internal false, // no wait nil, // arguments ) if err != nil { return err }
Далее инициализируются три очереди:
args := amqp.Table{"x-dead-letter-exchange": "VideoParserRetryExchange"} queue, err = ch.QueueDeclare( "VideoParserWorkerQueue", // name true, // durable - flush to disk false, // delete when unused false, // exclusive - only accessible by the connection that declares false, // no-wait - the queue will assume to be declared on the server args, // arguments - ) if err != nil { return err } args = amqp.Table{"x-dead-letter-exchange": "VideoParserExchange", "x-message-ttl": 60000} queue, err = ch.QueueDeclare( "VideoParserWorkerRetryQueue", // name true, // durable - flush to disk false, // delete when unused false, // exclusive - only accessible by the connection that declares false, // no-wait - the queue will assume to be declared on the server args, // arguments - ) if err != nil { return err } queue, err = ch.QueueDeclare( "VideoParserArchiveQueue", // name true, // durable - flush to disk false, // delete when unused false, // exclusive - only accessible by the connection that declares false, // no-wait - the queue will assume to be declared on the server nil, // arguments - ) if err != nil { return err }
И далее очереди связываются с соответствующими Exchange:
VideoParserExchange с очередями VideoParserWorkerQueue и
VideoParserArchiveQueue, а VideoParserRetryExchange с очередью
VideoParserWorkerRetryQueue:
err = ch.QueueBind("VideoParserWorkerQueue", "*", "VideoParserExchange", false, nil) if err != nil { return err } err = ch.QueueBind("VideoParserArchiveQueue", "*", "VideoParserExchange", false, nil) if err != nil { return err } err = ch.QueueBind("VideoParserWorkerRetryQueue", "*", "VideoParserRetryExchange", false, nil) if err != nil { return err } return nil}
Переходим к файлам миграций БД. Они находятся в отдельной папке
migrations:
Devices_up.sql предназначен для создания таблицы requests. В ней
содержатся следующие поля:
-
id уникальный идентификатор запроса;
-
name уникальное имя, которое мы будем передавать в API при
создании нового запроса и в дальнейшем использовать его для поиска
нужного запроса;
-
description описание запроса;
-
video_url ссылка на исходное видео на YouTube, в котором
необходимо распарсить текст;
-
text_url ссылка на место хранения результирующего текстового
файла в S3;
-
processed логический признак того, что обработка запроса успешно
завершена;
-
archived логический признак того, что запись таблицы
архивирована. Будем использовать вместо физического удаления для
сохранения истории;
-
created_at, updated_at временные метки для сохранения времени
создания и последнего редактирования, соответственно.
Итак, создаем таблицу requests:
CREATE TABLE IF NOT EXISTS requests ( id SERIAL, name VARCHAR(256), description VARCHAR(2048), video_url VARCHAR(64), text_url VARCHAR(64), processed BOOL DEFAULT FALSE, archived BOOL DEFAULT FALSE, created_at TIMESTAMP DEFAULT now(), updated_at TIMESTAMP DEFAULT null, UNIQUE(name));
В devices_down.sql описывается удаление таблицы requests:
DROP TABLE requests;
Переходим к файлу
requests.go. В нем содержатся функции, которые обрабатывают
запросы:
-
addRequest для добавления запроса;
-
updRequest для редактирования запроса;
-
delRequest для удаления запроса;
-
getRequest для получения запроса по имени;
-
getRequests для получения всех запросов.
Все функции довольно простые, в них выполняется проверка входных
данных и отправка SQL-запроса в PostgreSQL. Поэтому приведем только
фрагмент кода основной функции addRequest. Остальные функции можно
посмотреть по ссылке выше.
Здесь происходит попытка отправить сообщение в
VideoParserExchange, вывод сообщения в случае ошибки и добавление
новой записи в таблицу requests, рассмотренную выше:
func addRequest(w http.ResponseWriter, r *http.Request) { // Parsing event req := postRequestRequest{} err := json.NewDecoder(r.Body).Decode(&req) if err != nil { log.Printf("WARNING: Can't parse incoming request: %s\n", err) returnResponse(400, "Can't parse json", nil, w) return } request := Request{} if req.Name == nil { returnResponse(400, "name can't be null", nil, w) return } request.Name = *req.Name if req.Description != nil { request.Description = *req.Description } if req.Processed != nil { request.Processed = *req.Processed } if req.VideoURL != nil { request.VideoURL = *req.VideoURL } if req.TextURL != nil { request.TextURL = *req.TextURL } // Publishing data to rabbitmq msg, err := json.Marshal(request) if err != nil { log.Printf("ERROR: Marshaling request: %s\n", err) returnResponse(500, "Can't marshal request ", nil, w) return } err = ch.Publish( "VideoParserExchange", // exchange "", // routing key false, // mandatory - could return an error if there are no consumers or queue false, // immediate amqp.Publishing{ DeliveryMode: amqp.Persistent, ContentType: "application/json", Body: msg, }) if err != nil { log.Printf("ERROR: Publishing to rabbit: %s\n", err) returnResponse(500, "Can't publish to rabbit ", nil, w) return } stmt := `INSERT INTO requests (name, description, processed, video_url, text_url) VALUES ($1, $2, $3, $4, $5) RETURNING id` err = db.QueryRow(stmt, &request.Name, &request.Description, &request.Processed, &request.VideoURL, &request.TextURL).Scan(&request.ID) if err != nil { log.Printf("ERROR: Adding new request to database: %s\n", err) returnResponse(500, "Can't add new request ", nil, w) return } returnResponse(200, "Successfully added new request", nil, w)}
В завершение рассмотрим
Dockerfile, с помощью которого можно собрать приложение. Здесь
используется образ golang-alpine, выполняется статическая
компиляция, затем берется чистый alpine, куда переносится
приложение со всеми миграциями и необходимыми файлами:
FROM golang:1.15-alpine AS build# Installing requirementsRUN apk add --update git && \ rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*# Creating workdir and copying dependenciesWORKDIR /go/src/appCOPY . .# Installing dependenciesRUN go getENV CGO_ENABLED=0RUN go build -o api main.go requests.goFROM alpine:3.9.6RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \ apk add --update bash && \ rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*WORKDIR /appCOPY --from=build /go/src/app/api /app/apiCOPY ./migrations/ /app/migrations/CMD ["/app/api"]
Создание БД PostgreSQL в облаке MCS
Базу данных для хранения статуса обработки запросов на
конвертацию видео будем создавать из консоли управления облаком
MCS. Для этого нужно выбрать пункт меню Базы
данных и добавить БД PostgreSQL:
На первом шаге определяется конфигурация. Выберем последнюю
версию PostgreSQL и тип конфигурации Single: для среды Dev нам
достаточно единичного инстанса:
На следующем шаге указываем имя инстанса БД и выбираем
конфигурацию виртуальной машины. Нам достаточно 1 CPU и 2 ГБ
памяти. Для зоны доступности оставляем автоматический выбор:
В качестве диска выберем SSD размером 20 ГБ. Сеть можно создать
отдельную, мы возьмем текущую. Внешний IP назначать не будем: база
будет во внутренней сети. В настройках Firewall при необходимости
можно указать ограничения на доступ, нам пока они не нужны все
разрешаем. Создание реплики нам также не нужно. Ключ для доступа по
SSH создаем свой. И устанавливаем периодичность резервного
копирования раз в сутки:
На следующем шаге указываем имя БД, имя пользователя и
генерируем пароль:
Далее запускается процесс создания инстанса, который займет
некоторое время. После успешного создания параметры БД будут
выведены на экран, в том числе внутренний IP-адрес сети, который
впоследствии нам понадобится:
Установка RabbitMQ через Helm в Kubernetes
Для установки RabbitMQ воспользуемся Helm-чартом
bitnami/rabbitmq. Достоинство чартов в том, что не нужно
устанавливать по отдельности все необходимые сервису ресурсы: можно
установить их одновременно в рамках общего релиза. А при изменениях
в любом из ресурсов можно вынести новый релиз, в котором все
обновления будут собраны воедино.
Создадим папку helm, добавим в нее репозиторий bitnami и найдем
нужный нам Helm Chart bitnami/rabbitmq:
mkdir helmcd helmhelm repo add bitnami https://charts.bitnami.com/bitnamihelm search repo bitnami
Теперь мы нашли нужный чарт:
Копируем его имя, загружаем и распаковываем:
helm pull bitnami/rabbitmqtar zxv
Переходим в папку rabbitmq/templates. Здесь находятся все
ресурсы, которые нужно будет создать в Kubernetes для корректной
работы RabbitMQ: конфигурация, Ingress, сертификаты, сетевые
политики, сервисные аккаунты, секреты, правила Prometheus и так
далее. И Helm позволяет это сделать единой командой, без установки
каждого файла по отдельности:
Возвращаемся в родительскую папку helm, чтобы посмотреть
возможность настройки файла values.yaml. Скопируем содержимое
rabbitmq/values.yaml в наш собственный файл values.dev.yaml и
откроем его для редактирования:
cp rabbitmq/values.yaml ./values.dev.yamlvi values.dev.yaml
Так поступать рекомендуется всегда, так как настройки для разных
сред будут отличаться.
В данном файле содержится очень много параметров, которые можно
настраивать под нужды своего проекта: режим debug, плагины RabbitMQ
для подключения, необходимость включения TLS и memoryHighWatermark,
аутентификация через LDAP, количество реплик, nodeSelector для
создания RabbitMQ на нодах с определенной меткой, требования к CPU
и памяти и многое другое.
Нас в первую очередь интересуют настройки Ingress. Находим
секцию ingress, устанавливаем в enabled значение true и прописываем
в поле hostname имя rabbitmq.stage.kis.im. Эта настройка необходима
для внешнего доступа к RabbitMQ, без нее он будет доступен только
внутри кластера. Kis.im это мой существующий домен:
Далее переходим непосредственно к развертыванию RabbitMQ.
Создаем новый namespace stage и применяем к нему созданный файл
values.stage.yaml (изменив dev на stage в названии для
единообразия):
kubectl create ns stagehelm instal -n stage rabbitmq -f values.dev.yamlmv values.dev.yaml values. stage. yamlhelm install -n stage rabbitmq -f values.stage.yanl ./rabbitmq/
Вот, что получилось, когда Namespace создан:
После успешной установки можно посмотреть список подов и
сервисов в Namespace stage rabbitmq успешно добавлен. Он имеет
кластерный IP 10.254.178.84. Но так как наше приложение будет
находиться в том же Namespace, мы сможем обращаться к нему по имени
rabbitmq.
Еще один сервис rabbitmq-headless не имеет кластерного IP. Он
используется при добавлении нескольких RabbitMQ для их
автообнаружения и объединения в кластер с помощью kubectl -n
stage get svc
:
С помощью Helm можно получить дополнительные сведения о релизе:
время последнего обновления, статус, название чарта, версию
приложения, используем helm -n stage list
:
Кроме этого, можно посмотреть Persistent Volumes, выделенные
RabbitMQ, с помощью kubectl get pv
. В нашем случае
Volume имеет размер 8 ГБ и Storage Class csi-hdd:
При необходимости нужный Storage Class можно было прописать
непосредственно в YAML-файле:
Список всех возможных классов можно вывести командой
kubectl get storageclasses
:
Здесь важен параметр RECLAIMPOLICY
: в зависимости
от его значения при удалении запроса на данный ресурс (PVC,
Persistent Volume Claim) сам Persistent Volume будет удален или
сохранен для будущего использования.
Осталось обеспечить внешний доступ к нашему сервису. Проверяем
добавление ресурса Ingress для RabbitMQ командой kubectl -n
stage get ingress
:
Затем получаем внешний адрес Ingress Controller с помощью
kubectl -n ingress-nginx get svc
:
В Cloudflare прописываем DNS для RabbitMQ, связывая его внешний
Hostname и IP-адрес Ingress Controller:
После этого RabbitMQ становится доступен по адресу
rabbitmq.stage.kis.im:
Имя пользователя user. Пароль сохранился в переменные окружения
после развертывания RabbitMQ, его можно получить с помощью команды
env | grep RABBITMQ_PASSWORD.
Развертывание и предварительная проверка API
RabbitMQ мы развернули с помощью Helm. Для нашего приложения с
API в последующем мы также создадим собственный Helm Chart, но пока
посмотрим, как выполняется развертывание приложения вручную на
основе YAML-файлов.
Образ приложения мною уже создан при помощи Dockerfile, который
мы рассматривали ранее.
Далее определим необходимые ресурсы. Очевидно, что локальное
хранилище приложению не нужно, так как приложение уже
взаимодействует с PostgreSQL и RabbitMQ, размещенными в облаке.
Поэтому Persistent Volumes создавать не будем. Основные ресурсы,
которые нам потребуются, описывают файлы deployment.yaml,
ingress.yaml и svc.yaml:
Начнем с deployment.yaml. Здесь описывается ресурс Deployment.
Тут мы описываем шаблон пода, который будем запускать. Указываем,
что будем запускать контейнер с именем api, образ
vozerov/video-api:v1 (этот образ я уже
залил на hub.docker.com).
Далее в блоке env указываем переменные, используемые в нашем
API:
-
В переменной RABBIT_URI вводим сформированные при создании
RabbitMQ имя и пароль пользователя, название сервиса rabbitmq и
номер порта 5672 (имя сервиса можно проверить с помощью команды
kubectl -n stage get svc).
-
В переменной LISTEN устанавливаем номер порта 8080.
-
В переменной PGSQL_URI заполняем сформированные при создании
PostgreSQL имя и пароль пользователя, внутренний адрес БД
10.0.0.10, номер порта 5432 и название БД vc-dev. Все параметры БД
можно найти в консоли управления облаком.

deployment.yaml: описываем шаблон пода
По хорошему, пароли нельзя хранить тут в открытом виде. Но как я
уже говорил ранее, это MPV, и для упрощения мы сейчас сделаем
так.
Применяем сформированный файл:
kubectl -n stage apply -f deployment.yamlkubectl -n stage get deploy
Video-api создан:
И проверяем создание нового пода с помощью kubectl -n
stage get pods
:
После успешного применения deployment.yaml можно зайти в
RabbitMQ и убедиться в создании всех необходимых очередей и
Exchange.

Созданные очереди

Созданные Exchange
Следующий ресурс, который нам необходимо добавить для доступа к
сервису извне это Service. Он описывается в файле svc.yaml. Мы
указываем, что приложение video-api будет принимать входящие
соединения на порт 8080 и пробрасывать их в контейнер на порт 8080.
Применяем svc.yaml стандартной командой kubectl apply -n
stage -f svc.yaml
:
Последний ресурс, который необходим для нашего сервиса Ingress.
В файле ingress.yaml мы указываем правила, по которым нужно
направлять запросы к сервису. Заполняем внешнее имя
api.stage.kis.im и в блоке path указываем, что все корневые запросы
направляем на сервис video-api-svc, созданный на прошлом шаге.
Применяем сформированный файл kubectl apply -n stage -f
Ingress.yaml
:
Убеждаемся в добавлении Ingress для нашего сервиса с помощью
kubectl -n stage get ingress:
Затем добавляем запись в DNS аналогично тому, как делали это
ранее для RabbitMQ:
Теперь можно провести первое тестирование API, используя
отправку запросов через curl. В заголовках всех запросов нужно
передавать X-API-KEY со значением токена из кода программы
main.go.
Для начала с помощью метода GET получим список всех записей
requests:
curl -H 'X-API-KEY: 804b95f13b714ee9912b19861faf3d25' -s http://api.stage.kis.im/requests | jq .
На текущий момент он пуст:
Отправим новый запрос на конвертацию видео, используя метод
POST. В имени запроса (name) укажем test1. В ссылке на видео
(video_url) введем тестовое значение, так как у нас пока нет
обработчиков Worker:
curl -X POST -d '{"name": "test1", "video_url": "https://google.com" }' -H 'X-API-KEY: 804b95f13b714ee9912b19861faf3d25' -s http://api.stage.kis.im/requests | jq .
Запрос успешно создан:
Далее можно получить запрос по имени test1 и убедиться в наличии
всех переданных при создании параметров:
curl -H 'X-API-KEY: 804b95f13b714ee9912b19861faf3d25' -s http://api.stage.kis.im/requests/request1 | jq .
Запрос создан, все параметры верные:
В очереди RabbitMQ сообщение также будет добавлено. Заходим в
очередь:
Видим сообщение:
Осталось зайти в базу PostgreSQL и проверить ее структуру.
Внешний доступ мы не настраивали поэтому можно подключиться,
например, через psql из отдельно запущенного пода. Мы видим наличие
таблицы requests, а в ней добавленный нами запрос:
Таким образом, проверка работы API пройдена.
На этом пока все, во второй части статьи мы настроим и запустим
приложение для преобразования аудио в текст, сохраним результат и
настроим автомасштабирование нод в кластере.
Новым пользователям платформы Mail.ru Cloud Solutions доступны
3000 бонусов после полной верификации аккаунта. Вы сможете
повторить сценарий из статьи или попробовать другие облачные
сервисы.
И обязательно вступайте
в сообществоRebrain в Telegram там
постоянно разбирают различные проблемы и задачи из сферы Devops,
обсуждают вещи, которые пригодятся и на собеседованиях, и в
работе.
Что еще почитать по теме:
-
Как развернуть кластер Kubernetes на платформе MCS.
-
Запускаем etcd-кластер для Kubernetes.
-
Как устроен Kubernetes aaS на платформе Mail.ru Cloud
Solutions.