Деплоим проект на Kubernetes в Mail.ru Cloud Solutions. Часть 1: архитектура приложения, запуск Kubernetes и RabbitMQ +28


AliExpress RU&CIS

О 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.

Сценарий взаимодействия выбранных компонентов включает в себя следующие шаги:

  1. Клиент отправляет на API-сервер запрос POST, передавая в теле запроса имя и URL видео на YouTube, которое необходимо перевести в текст.

  2. API-сервер формирует сообщение с полученными параметрами и передает его в очередь RabbitMQ.

  3. API-сервер сохраняет информацию о полученном запросе на конвертацию видео в базе данных PostgreSQL. Статус обработки запроса по умолчанию равен false.

  4. API-сервер информирует клиента об успешном завершении операции. Клиент может продолжать свою работу, не дожидаясь конвертации видео.

  5. Свободный обработчик Worker извлекает сообщение из очереди RabbitMQ.

  6. Получив сообщение, Worker выполняет его обработку: загружает видео по указанному URL, получает из него аудио и переводит при помощи стороннего ПО в текст.

  7. Обработав видео, Worker сохраняет транскрипт видео в хранилище S3.

  8. Worker отправляет в API-сервер информацию об успешной обработке запроса с исходным именем. В запросе передается статус обработки, равный true, и ссылка на текстовый файл в S3. Endpoint для отправки статуса обработки запросов можно либо жестко прописывать в environment-переменных обработчика Worker, либо передавать его в теле сообщений наряду с другими параметрами. В нашем MVP будет реализован первый вариант. То есть обработчикам будет известно, какой API вызвать для обновления статуса запросов.

  9. API-сервер обновляет полученную от Worker информацию о запросе в базе данных PostgreSQL. Альтернативный вариант — можно настроить обновление базы данных непосредственно из обработчиков Worker, однако это потребует знания структуры БД с их стороны, что чревато проблемами при миграциях БД. Поэтому в нашем приложении взаимодействие с БД будет происходить исключительно через API-сервер.

  10. Клиент спустя некоторое время после отправки исходного видео запрашивает статус его обработки, передавая в API-сервер имя исходного запроса.

  11. API-сервер извлекает данные о запросе из PostgreSQL по полученному имени.

  12. API-сервер получает информацию о запросе из PostgreSQL.

  13. 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 минут.

При успешном добавлении кластера на экране отобразится информация о его параметрах:

Для последующей работы с кластером необходимо:

  1. Установить локальный клиент kubectl и запустить его.

  2. Экспортировать в локальный клиент конфигурационный файл созданного кластера с расширением .yaml командой export KUBECONFIG=<путь к файлу>.

  3. Для безопасного подключения к кластеру запустить proxy-сервер командой kubectl proxy.

Эта инструкция отображается под списком параметров кластера после его добавления.

У нас kubectl установлен — поэтому берем из загрузок сформированный конфигурационный файл kub-vc-dev_kubeconfig.yaml и экспортируем его в kubectl:

После экспорта конфигурационного файла можно убедиться в работоспособности кластера:

  1. Сначала смотрим доступные контексты: kubectl config get-contexts

    Видим, что у нас создался кластер kub-vc-dev:

  2. Смотрим доступные ноды: kubectl get nodes

    В кластере создались две ноды — master и workload:

  3. Смотрим доступные Namespace: kubectl get ns

    Получаем ответ:

  4. Смотрим доступные поды: kubectl -n ingress-nginx get pods

    В Namespace ingress-nginx запущены поды для Nginx Controller:

  5. Смотрим доступные сервисы: 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-сервера
Структура 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.

Развертывание и предварительная проверка API

RabbitMQ мы развернули с помощью Helm. Для нашего приложения с API в последующем мы также создадим собственный Helm Chart, но пока посмотрим, как выполняется развертывание приложения вручную на основе YAML-файлов.

Образ приложения мною уже создан при помощи Dockerfile, который мы рассматривали ранее.

Далее определим необходимые ресурсы. Очевидно, что локальное хранилище приложению не нужно, так как приложение уже взаимодействует с PostgreSQL и RabbitMQ, размещенными в облаке. Поэтому Persistent Volumes создавать не будем. Основные ресурсы, которые нам потребуются, описывают файлы deployment.yaml, ingress.yaml и svc.yaml:

Начнем с deployment.yaml. Здесь описывается ресурс Deployment. Тут мы описываем шаблон пода, который будем запускать. Указываем, что будем запускать контейнер с именем api, образ vozerov/video-api:v1 (этот образ я уже залил на hub.docker.com).

Далее в блоке env указываем переменные, используемые в нашем API:

  • В переменной RABBIT_URI вводим сформированные при создании RabbitMQ имя и пароль пользователя, название сервиса rabbitmq и номер порта 5672 (имя сервиса можно проверить с помощью команды kubectl -n stage get svc).

  • В переменной LISTEN устанавливаем номер порта 8080.

  • В переменной PGSQL_URI заполняем сформированные при создании PostgreSQL имя и пароль пользователя, внутренний адрес БД 10.0.0.10, номер порта 5432 и название БД vc-dev. Все параметры БД можно найти в консоли управления облаком.

deployment.yaml: описываем шаблон пода
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
Созданные 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, обсуждают вещи, которые пригодятся и на собеседованиях, и в работе.

Что еще почитать по теме:

  1. Как развернуть кластер Kubernetes на платформе MCS.

  2. Запускаем etcd-кластер для Kubernetes.

  3. Как устроен Kubernetes aaS на платформе Mail.ru Cloud Solutions.




Комментарии (5):

  1. unseriously
    /#22899548

    1. Почему используется Cloudflare, у Mail.ru нет своего CDN?
    2. Я не зарегестрирован на Mail.ru Cloud solutions, смогу ли я зарегистрироваться и сразу приступить к выполнению этого урока? Меня смущает фраза «после полной верификации аккаунта»? Или логичнее будет воспользоваться услугами другого облачного провайдера, типа AWS?

    • ovi
      /#22901670

      1. В данном случае мы использовали cloudflare как dns для нашей зоны. И включили проксирование чтобы SSL автоматически подрубился. У MCS есть свой CDN — mcs.mail.ru/cdn и его как раз можно будет использовать с s3 для отдачи статического контента.

      2. Для верификации нужно подтвердить почту, телефон и привязать карту. Такие требования вроде у всех облачных провайдеров (не помню, регистрировался везде сто лет назад).

  2. scruff
    /#22900148

    Постоянно смотрю вэбинары Василия Озерова и его команды — очень хорошая подача, особенно про Linux. Хотелось бы чтобы больше делался фокус на DevOps for dummies )))). Я далёк от около-девопсной темы к сожалению, но хотел бы изучють это направление. Хорошо бы если бы понизили «порог вхождения» на ваши девопс-вэбинары, например с чего начать Докер или как развернуть кластер кубернетис on-premises — что-то вроде такой элементарщины. Надеюсь услышите ))))

    • ovi
      /#22901350

      Доброго!

      Спасибо за позитивный фидбек! По поводу начальных вещей с удовольствием прикрутим — в конце апреля будем делать Kubernetes с установкой и базовой настройкой. Следите за анонсами и подключайтесь :)

      • scruff
        /#22901572

        Услышали!!! Ой спасибо!!! Давайте-давайте — больше азов. Интересен также базовый траблшутинг докера и кубера — от «лагает и тупит» до «лежит и хз как это чинить» ))))