О 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.
Остановимся подробнее на содержимом main.go.
Вначале импортируем нужные зависимости. В первую очередь, это migrate для автоматического осуществления миграций, database/sql для работы с базами данных, go-env для работы с переменными окружения, web-фреймворк Gorilla и AMQP для работы с RabbitMQ:
package main
import (
"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 configuration
log.Printf("INFO: Getting environment variables\n")
cnf := environment{}
_, err = env.UnmarshalFromEnviron(&cnf)
if err != nil {
log.Fatal(err)
}
// Connecting to database
log.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 migrations
driver, 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 rabbitmq
conn, 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 query
log.Printf("INFO: Starting listening on %s\n", cnf.Listen)
router := mux.NewRouter().StrictSlash(true)
// PROJECTS
router.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 requirements
RUN apk add --update git && \
rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/*
# Creating workdir and copying dependencies
WORKDIR /go/src/app
COPY . .
# Installing dependencies
RUN go get
ENV CGO_ENABLED=0
RUN go build -o api main.go requests.go
FROM alpine:3.9.6
RUN 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 /app
COPY --from=build /go/src/app/api /app/api
COPY ./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 helm
cd helm
helm repo add bitnami https://charts.bitnami.com/bitnami
helm search repo bitnami
Теперь мы нашли нужный чарт:
Копируем его имя, загружаем и распаковываем:
helm pull bitnami/rabbitmq
tar zxv
Переходим в папку rabbitmq/templates. Здесь находятся все ресурсы, которые нужно будет создать в Kubernetes для корректной работы RabbitMQ: конфигурация, Ingress, сертификаты, сетевые политики, сервисные аккаунты, секреты, правила Prometheus и так далее. И Helm позволяет это сделать единой командой, без установки каждого файла по отдельности:
Возвращаемся в родительскую папку helm, чтобы посмотреть возможность настройки файла values.yaml. Скопируем содержимое rabbitmq/values.yaml в наш собственный файл values.dev.yaml и откроем его для редактирования:
cp rabbitmq/values.yaml ./values.dev.yaml
vi 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 stage
helm instal -n stage rabbitmq -f values.dev.yaml
mv values.dev.yaml values. stage. yaml
helm 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. Все параметры БД можно найти в консоли управления облаком.
По хорошему, пароли нельзя хранить тут в открытом виде. Но как я уже говорил ранее, это MPV, и для упрощения мы сейчас сделаем так.
Применяем сформированный файл:
kubectl -n stage apply -f deployment.yaml
kubectl -n stage get deploy
Video-api создан:
И проверяем создание нового пода с помощью kubectl -n stage get pods
:
После успешного применения deployment.yaml можно зайти в RabbitMQ и убедиться в создании всех необходимых очередей и 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, обсуждают вещи, которые пригодятся и на собеседованиях, и в работе.
Что еще почитать по теме: