[Облачные вычисления, DevOps, Kubernetes] Деплоим проект на Kubernetes в Mail.ru Cloud Solutions. Часть 1: архитектура приложения, запуск Kubernetes и RabbitMQ

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
07-Апр-2021 22:32


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

Упрощенная схема архитектуры будущего приложенияНастройка кластера Kubernetes в облаке MCSНачинаем с создания кластера Kubernetes. Для этого в панели управления облаком MCS необходимо выбрать пункт меню «Контейнеры» — «Кластеры Kubernetes» и добавить новый кластер.На первом шаге настраивается конфигурация будущего кластера. Можно выбрать тип среды и один или несколько предустановленных сервисов. Мы выберем среду Dev и сразу добавим Ingress Controller Nginx — для управления внешним доступом к кластеру:
На следующем шаге вводим название кластера и выбираем тип виртуальной машины для ноды Master. Оставим стандартную конфигурацию с 2 CPU и 4 ГБ памяти. Далее можно указать зону доступности — мы оставим для нее автоматическое заполнение:
Далее на этом же шаге выбирается тип и размер диска. Нам достаточно HDD размером 20 Гб. Оставляем одну Master-ноду, выбираем предварительно добавленную подсеть и назначаем внешний IP для удобного доступа к кластеру извне:
На следующем шаге создаются группы рабочих узлов. В рамках проекта нам потребуются две группы. Сейчас создадим первую для развертывания API и RabbitMQ, а впоследствии добавим еще одну, для обработчиков Worker.Вводим название группы узлов и указываем конфигурацию: 2 CPU и 4ГБ памяти. Для зоны доступности вновь выбираем автоматический выбор:
Чтобы обеспечить работу RabbitMQ, выбираем более производительный тип дисков — SSD размером 50 ГБ. Оставляем один узел, автомасштабирование пока не указываем — его рассмотрим позднее на примере другой группы узлов:
На последнем шаге запускается процесс формирования кластера, который может занять некоторое время: от 5 до 20 минут.При успешном добавлении кластера на экране отобразится информация о его параметрах:
Для последующей работы с кластером необходимо:
  • Установить локальный клиент kubectl и запустить его.
  • Экспортировать в локальный клиент конфигурационный файл созданного кластера с расширением .yaml командой export KUBECONFIG=<путь к файлу>.
  • Для безопасного подключения к кластеру запустить proxy-сервер командой kubectl proxy.
Эта инструкция отображается под списком параметров кластера после его добавления.У нас kubectl установлен — поэтому берем из загрузок сформированный конфигурационный файл kub-vc-dev_kubeconfig.yaml и экспортируем его в kubectl:
После экспорта конфигурационного файла можно убедиться в работоспособности кластера:
  • Сначала смотрим доступные контексты: kubectl config get-contexts Видим, что у нас создался кластер kub-vc-dev:
  • Смотрим доступные ноды: kubectl get nodes В кластере создались две ноды — master и workload:
  • Смотрим доступные Namespace: kubectl get ns Получаем ответ:
  • Смотрим доступные поды: kubectl -n ingress-nginx get pods В Namespace ingress-nginx запущены поды для Nginx Controller:
  • Смотрим доступные сервисы: kubectl -n ingress-nginx get svс
В списке сервисов также отображается Nginx Controller, для которого указан внешний адрес, который мы сможем прописывать в DNS, чтобы попадать в наши сервисы извне:
Разработка API-сервера на GoСледующий шаг — написать API для отправки запросов на конвертацию видео и получения статуса их обработки. С полной версией исходного кода можно ознакомиться здесь.Ниже отображена структура проекта. Это стандартное Go-приложение. В файлах go.mod, go.sum описываются зависимости, в папке migrations — миграции для базы данных PostgreSQL. В main.go содержится основная логика программы, в requests.go — реализация API на добавление, редактирование, удаление и выборку запросов. И есть Dockerfile.
Структура API-сервераОстановимся подробнее на содержимом main.go.Вначале импортируем нужные зависимости. В первую очередь, это migrate для автоматического осуществления миграций, database/sql для работы с базами данных, go-env для работы с переменными окружения, web-фреймворк Gorilla и AMQP для работы с RabbitMQ:
package 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, обсуждают вещи, которые пригодятся и на собеседованиях, и в работе.
Что еще почитать по теме:
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_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-Ноя 13:15
Часовой пояс: UTC + 5