[Облачные вычисления, DevOps, Kubernetes] Деплоим проект на Kubernetes в Mail.ru Cloud Solutions. Часть 1: архитектура приложения, запуск Kubernetes и RabbitMQ
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
О 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.
Выбор архитектуры приложенияОпределимся с архитектурой будущего приложения. В первую очередь нам потребуется 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 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.Развертывание и предварительная проверка APIRabbitMQ мы развернули с помощью 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.yaml
kubectl -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.
===========
Источник:
habr.com
===========
Похожие новости:
- [Системное администрирование, DevOps] Давайте обсудим мониторинг
- [Системное администрирование, *nix, Серверное администрирование, DevOps] Худшие из так называемых «лучших практик» для Docker (перевод)
- [Облачные вычисления, Облачные сервисы, Будущее здесь, IT-компании] Зачем хоккею облака
- [Карьера в IT-индустрии, Atlassian, Интервью] «Не жалко людей, которых уволили благодаря твоей работе?» — интервью с Андреем Маркеловым, инженером Atlassian
- [Облачные вычисления, DevOps, Kubernetes] 11 Kubernetes implementation mistakes – and how to avoid them
- [Системы сборки, DevOps, Gradle] Сборка (CI/CD) не-JVM проектов с использованием gradle/kotlin
- [Системы сборки, DevOps, Gradle] Building projects (CI/CD), instruments
- [Системы сборки, DevOps, Gradle] Сборка не JVM-проектов, инструменты
- [Системное администрирование, .NET, DevOps] Обновление процесса CI/CD: год спустя
- [Системы сборки] Сборка (CI/CD) проектов
Теги для поиска: #_oblachnye_vychislenija (Облачные вычисления), #_devops, #_kubernetes, #_kubernetes, #_k8s, #_kontejnerizatsija (контейнеризация), #_devops, #_mail.ru_cloud_solutions, #_blog_kompanii_mail.ru_group (
Блог компании Mail.ru Group
), #_blog_kompanii_rebrein (
Блог компании Ребреин
), #_oblachnye_vychislenija (
Облачные вычисления
), #_devops, #_kubernetes
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:21
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
О 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. Выбор архитектуры приложенияОпределимся с архитектурой будущего приложения. В первую очередь нам потребуется API, к которому будет обращаться клиентское приложение. Будем использовать стандартные форматы: HTTPS и JSON. В JSON необходимо передавать URL видео, а также некоторый идентификатор или уникальное имя запроса — для возможности отслеживания его статуса.Следующий необходимый компонент — очередь сообщений. Очевидно, что обработку видео не получится проводить в real-time режиме. Поэтому будем использовать RabbitMQ для асинхронной обработки.Далее нам потребуются обработчики, которые будут читать сообщения из очереди и заниматься непосредственной конвертацией запрошенных видео в текст. Назовем их Worker. Для транскрибации будем использовать не внешнее API, а какую-нибудь библиотеку, установленную локально. Так как для этого потребуются ресурсы, обязательно настроим автомасштабирование в кластере, чтобы число обработчиков изменялось пропорционально количеству сообщений в очереди.Для сохранения текстовых расшифровок видео, которые будут формировать обработчики Worker, потребуется хранилище. Будем использовать S3, которое идеально подходит для хранения неструктурированных данных в облаке. Наконец, чтобы иметь возможность получать статус обработки запросов, их необходимо где-то сохранять. Для этого выберем обычную базу PostgreSQL.Сценарий взаимодействия выбранных компонентов включает в себя следующие шаги:
Упрощенная схема архитектуры будущего приложенияНастройка кластера 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 минут.При успешном добавлении кластера на экране отобразится информация о его параметрах: Для последующей работы с кластером необходимо:
После экспорта конфигурационного файла можно убедиться в работоспособности кластера:
Разработка 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 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" ) type environment struct {
PgsqlURI string `env:"PGSQL_URI"` Listen string `env:"LISTEN"` RabbitURI string `env:"RABBIT_URI"` } 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() // 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) } // 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)) 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) }) }
Архитектура очередей сообщенийЦель такого решения — отложить попытки отправки сообщений на вышедшие из строя 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 } 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 } Devices_up.sql предназначен для создания таблицы 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) ); DROP TABLE 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) } 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 и тип конфигурации 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 Возвращаемся в родительскую папку helm, чтобы посмотреть возможность настройки файла values.yaml. Скопируем содержимое rabbitmq/values.yaml в наш собственный файл values.dev.yaml и откроем его для редактирования: cp rabbitmq/values.yaml ./values.dev.yaml
vi values.dev.yaml Далее переходим непосредственно к развертыванию 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 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.Развертывание и предварительная проверка APIRabbitMQ мы развернули с помощью 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:
deployment.yaml: описываем шаблон подаПо хорошему, пароли нельзя хранить тут в открытом виде. Но как я уже говорил ранее, это MPV, и для упрощения мы сейчас сделаем так.Применяем сформированный файл: kubectl -n stage apply -f deployment.yaml
kubectl -n stage get deploy И проверяем создание нового пода с помощью 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, обсуждают вещи, которые пригодятся и на собеседованиях, и в работе.
=========== Источник: habr.com =========== Похожие новости:
Блог компании Mail.ru Group ), #_blog_kompanii_rebrein ( Блог компании Ребреин ), #_oblachnye_vychislenija ( Облачные вычисления ), #_devops, #_kubernetes |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:21
Часовой пояс: UTC + 5