
О 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, обсуждают вещи, которые пригодятся и на собеседованиях, и в работе.
Что еще почитать по теме:
