golang

Как задеплоить своего телеграм-бота (почти) бесплатно — Quickguide в облачный Serverless

  • четверг, 25 мая 2023 г. в 00:00:18
https://habr.com/ru/companies/yandex_cloud_and_infra/articles/735988/

Всем привет! Меня зовут Антон Брехов. Я инженер в Yandex Cloud. Сегодня хочу рассказать о том, как дешевле всего задеплоить своего телеграм-бота. Возможно, этот опыт пригодится и для других решений.

Готовых фреймворков для телеграм-ботов уже достаточно много на любых языках. Однако после написания кода встает вопрос: а как теперь заставить бота работать постоянно, сделать доступным 24/7?

Новички оставляют персональный компьютер работающим и опрашивают сервер телеграма с некоторой частотой. У опытных, скорее всего, есть свой VPS-сервер с reverse proxy для деплоя приложений. Первое решение не является высокодоступным – всё-таки персональный компьютер не предназначен для круглосуточной работы (чаще всего он не подключен к источнику бесперебойного питания, может иметь проблемы с доступом в интернет и т.д.). А отдельный сервер, даже в облаке, — это слишком дорого для деплоя одного бота. Стоимость одного VPS в среднем — от 5$ в месяц.

В статье расскажу, как работает альтернативное решение на базе наших Serverless-сервисов, которое использую сам.

Вот так выглядит схема в случае бота с настроенным вебхуком. Пользователь отправляет сообщение боту в телеграм. Тот стучится на настроенный вебхук. Запускается написанная логика — отправляем ответ.

Здесь мы используем несколько продуктов:

  1. Основное — Serverless Containers. Это сервис, позволяющий запускать свои контейнеры по вызову HTTP или других триггеров. Подробнее тут. А также репозиторий образов контейнеров Container Registry.

  2. Следующий сервис — API Gateway. Он позволяет бессерверно предоставлять в открытый доступ HTTP (а с недавнего времени ещё и WS) эндпоинт для доступа к вашим облачным функциям, бессерверным контейнерам, другим сервисами Yandex Cloud и не только.

  3. Container Registry — собственный registry для контейнеров с поддержкой сканера на уязвимости.

  4. СУБД YDB в режиме Serverless — мультитенантная реляционная СУБД внутренней разработки Яндекса. Здесь важно, что не придётся платить за ресурсы, в отличие от сервисов Managed Service for MySQL или Managed Service for PostgreSQL. Решение отлично подходит в том числе для прототипов, потому что оплата идёт только за полезную нагрузку (количество выполненных запросов).

После создания Serverless-контейнера можно использовать в качестве вебхука прикрепленный к нему URL (если вы сделали его публично доступным). Однако рекомендую использовать перед контейнером API Gateway.

Если вам нужно после прихода запроса от серверов телеграма сделать обратный запрос (к примеру, скачать присланный в бота файл), этот запрос будет воспринят как конечный ответ приложения Serverless. После этого контейнер будет помечен как выполненный и остановится через несколько секунд. Если же между контейнером и телеграмом будет прослойка в виде API Gateway, такого не произойдет.

У YDB свой синтаксис YQL, схожий с привычным всем SQL. Но различий предостаточно. С удовольствием бы использовал его as is, если бы к нему был готовый ORM driver (как, например, PostgreSQL driver для gorm.io ).

YDB в Serverless-режиме имеет dynamodb compatible API, то есть можно брать API совместимую библиотеку и работать с YDB (правда, это уже не реляционная модель, но для прототипа сойдёт!).

Так и сделаем: https://github.com/guregu/dynamo

Вот пример модели пользователя телеграм.

package user

import "time"

type User struct {
	UserID   int64     `dynamo:",hash"`
	Created  time.Time `dynamo:",range"`
	Username string    `dynamo:""`
}

В качестве сервера использую echo от labstack (очень нравится синтаксис).

package main

import (
	"log"
	"os"
	"yc-qr-bot/pkg/agent"

	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
	"github.com/joho/godotenv"
	"github.com/labstack/echo/v4"
)

var Version string

func main() {
	log.Printf("Version: %v\\n", Version)
	godotenv.Load()

	bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN"))
	if err != nil {
		panic(err)
	}

	whInfo, _ := bot.GetWebhookInfo()
	log.Printf("whInfo: %#v\\n", whInfo)
	a := agent.New(bot)
	e := echo.New()
	e.POST("/", a.HandleUpdate)
	e.Start(":" + os.Getenv("PORT"))
}

Детали самой реализации обработки можно найти в репозитории .

Самое трудное во всей схеме для новичка — задеплоить решение. Для этого написал Makefile для автоматизации.

include .env

create:
	yc serverless container create --name $(SERVERLESS_CONTAINER_NAME)
	yc serverless container allow-unauthenticated-invoke --name  $(SERVERLESS_CONTAINER_NAME)

create_gw_spec:
	$(shell sed "s/SERVERLESS_CONTAINER_ID/${SERVERLESS_CONTAINER_ID}/;s/SERVICE_ACCOUNT_ID/${SERVICE_ACCOUNT_ID}/" api-gw.yaml.example > api-gw.yaml)
create_gw: create_gw_spec
	yc serverless api-gateway create --name $(SERVERLESS_CONTAINER_NAME) --spec api-gw.yaml
webhook_info:
	curl --request POST --url "<https://api.telegram.org/bot$(TELEGRAM_APITOKEN)/getWebhookInfo>"

webhook_delete:
	curl --request POST --url "<https://api.telegram.org/bot$(TELEGRAM_APITOKEN)/deleteWebhook>"

webhook_create: webhook_delete
	curl --request POST --url "<https://api.telegram.org/bot$(TELEGRAM_APITOKEN)/setWebhook>" --header 'content-type: application/json' --data "{\\"url\\": \\"$(SERVERLESS_APIGW_URL)\\"}"

build: webhook_create
	docker build -t cr.yandex/$(YC_IMAGE_REGISTRY_ID)/$(SERVERLESS_CONTAINER_NAME) .

push: build
	docker push cr.yandex/$(YC_IMAGE_REGISTRY_ID)/$(SERVERLESS_CONTAINER_NAME)

deploy: push
	$(shell sed 's/=.*/=/' .env > .env.example)
	yc serverless container revision deploy --container-name $(SERVERLESS_CONTAINER_NAME) --image 'cr.yandex/$(YC_IMAGE_REGISTRY_ID)/$(SERVERLESS_CONTAINER_NAME):latest' --service-account-id $(SERVICE_ACCOUNT_ID)  --environment='$(shell tr '\\n' ',' < .env)' --core-fraction 5 --execution-timeout $(SERVERLESS_CONTAINER_EXEC_TIMEOUT)

all: deploy

Будем использовать утилиту yc CLI. С помощью неё можно управлять ресурсами Yandex Cloud. Установите и инициализируете yc.

Скопируйте .env.example в .env

TELEGRAM_APITOKEN=
YC_IMAGE_REGISTRY_ID=
SERVICE_ACCOUNT_ID=
SERVERLESS_CONTAINER_EXEC_TIMEOUT=
SERVERLESS_CONTAINER_NAME=
SERVERLESS_CONTAINER_ID=
SERVERLESS_CONTAINER_URL=
SERVERLESS_APIGW_URL=
AWS_DEFAULT_REGION=
YDB_ENDPOINT=

Заполните четыре первые переменные.

⚡ Создайте сервисный аккаунт и дайте ему права serverless.containers.invoker container-registry.images.puller ydb.admin а также registry контейнеров .

Потом выполняем: make create

Копируем id созданного Serverless-контейнера, и дописываем в .env

Создаем теперь API Gateway: make create_gw

Копируем URL для доступа к созданному API Gateway и копируем обратно в .env (SERVERLESS_APIGW_URL).

Устанавливаем вебхук для бота: make webhook_create

И, наконец, собираем образ и отправляем в Docker Registry: make deploy

Отправляем в бота сообщение и смотрим в логи Serverless-контейнера.

Если нагрузка на бота незначительная, то стоимость решения будет копеечной. Объясняю почему. Согласно документации первые 1 млн вызовов в месяц для сервиса Serverless Containers — бесплатные. API Gateway — бесплатно первые 100к вызовов ежемесячно. Такая же ситуация с Serverless YDB — 1 миллион Request Units (RU) в месяц бесплатно.

Стоимость размещения моих ботов в месяц не превышает 10 рублей. По сути я плачу только за образы, которые храню в Container Registry.

Также воспользуюсь случаем и покажу своих ботов:

  • QR Bot: кодирование и декодирование QR-кодов.

  • Stenographer bot: конвертер аудиосообщений в текст. Сделано на базе Yandex SpeechKit

  • Levitan bot: конвертер текста в аудио. Присылайте ему текст — озвучит. Делал, чтобы  читать длинные новости в телеграме, пока еду за рулем.

  • Notion To-Do Adder bot: привяжите страницу в Notion и кидайте тудушки в бота — он будет добавлять их в конец страницы.