Это продолжение серий постов про «Бомжуем в Microsoft Azure» или как нам запускать сервисы в облаке и при этом не тратить много-много денег.
В Microsoft Azure есть такой сервис Azure Functions — это бессерверные вычисления. Суть его в том, что у вас есть какой-то код/функция, которая запустилась по какому-то триггеру, отработала, сделала что-то и спокойно умерла, ожидая, когда её вызовут еще раз. Такой Function as a Service. Вам не надо настраивать сервера, платформу или инфраструктуру, вы просто берёте свой код и запускаете где-то в облаке.
В Azure Functions существует несколько триггеров, с помощью которых вы можете запустить выполнение функции: HTTPTrigger, TimerTrigger, CosmosDBTrigger, BlobTrigger и т.д. Подробное описание всех триггеров есть в документации. Ажуровские функции поддерживают запуск кода на .net core, node.js, python, java и powershell core (preview) в Linux и Windows среде, включая собранные в docker-контейнерах.
Но почему я решил написать про Azure Functions. Знаете почему? Потому что выполнение функций стоит копейки. 1 миллион запусков в месяц обойдётся в 12,50 рублей. Да, да, двенадцать рублей и пятьдесят копеек. И вот, в один прекрасный субботний вечер, я подумал, а что если запустить телеграмовского бота внутри ажурных функций, но перед тем, как окунуться в мир ажура, надо рассказать про нюансы.
Для telegram, как и для любой платформы существует несколько способов запуска ботов: polling и webhook. Polling не требует сертификатов и публикации вашего приложения, он просто раз в секунду, например, идёт в API мессенджера и спрашивает «Есть чо?», если есть, то в код прилетает целая пачка объектов JSON, с которыми вы уже развлекаетесь. Это просто, не очень быстро работает при больших нагрузках и мессенджеры не любят, когда вы их регулярно пинаете. Webhook же работает наоборот, вы сообщаете мессенджеру endpoint, где живёт ваш фронт и когда в мессенджере происходит какая-то активность, то он просто присылает вам сообщения, но есть нюансы, так как вам требуется публикация вашего приложения, SSL-сертификат и FQDN имя.

В Azure Functions есть Consumption Plan — это оплата за потребление, но у него есть стандартное ограничение на выполнение функции — 5 минут, его можно расширить, но главная идея в том, что мы не будет крутить постоянно запущенный код для бота внутри Azure Functions, мы скажем телеграму, чтобы он сам триггерил функцию на запуск (тот самый Webhook) и вся магия уже будет происходить внутри кода. Для этого будем использовать HTTPTrigger.
Для Azure Functions можно писать и тестировать всё локально в VScode, а уже только потом заливать всё в облако. Нам необходимо создать функцию для анонимного HTTP триггера.
VScode создаст проект, все необходимые файлы и даст возможность запускать локально функции, как будто мы в облаке. ;) Открывайте Debug, запускаем и смотрим, что всё работает, видно, что реквесты летят и видно разные статусы.
Для работы с telegram будем использовать простую либу — pytelegrambotapi. Идём в @botfather в телеграме, получаем токен, тут ничего нового. Плюс нам понадобится ngrok для локального тестирования. Если вы еще с ним не сталкивались, то он просто поднимает тоннель для тестирования приложений локально, может пробросить порт, выдаёт вам домен, поддерживает https и пишет логи. Очень крутая и бесплатная штука.

В нашем проекте надо поправить несколько файлов. В requirements.txt добавляем pyTelegramBotAPI и requests.
host.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{ "version": "2.0", "logging": { "console": { "isEnabled": "true" } }, "extensions": { "http": { "routePrefix": "" } } } |
По-дефолту, в Azure Functions URL формируется https://…/api/ЧтоТоЕщё, а если хотите убрать /api/, то надо оставить routePrefix пустым.
function.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
{ "scriptFile": "__init__.py", "bindings": [ { "authLevel": "anonymous", "type": "httpTrigger", "direction": "in", "name": "req", "methods": [ "get", "post" ], "route": "tlg" }, { "type": "http", "direction": "out", "name": "$return" } ] } |
Это уже конфиг запуска функции. ScriptFile — файл запуска, authLevel в нашем случае анонимный, тип триггера, методы и route. Route добавляется к урлу функции, т.к. /api/ мы убрали, то вот route это и есть «ЧтоТоЕщё«.
__init__.py — это просто эхо бот, который отвечает на команду /start
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import logging import azure.functions as func import telebot from telebot import apihelper import time import os logger = telebot.logger telebot.logger.setLevel(logging.DEBUG) # import pysocks # apihelper.proxy = {'https': 'socks5://username:password@url-to-your-socks-server:1080'} # telegram bot token TOKEN = 'get-from-@botfather' bot = telebot.TeleBot(TOKEN, threaded=False) @bot.message_handler(commands=['start']) def start(message): bot.reply_to(message, 'Привет, ' + message.from_user.first_name) @bot.message_handler(func=lambda message: True, content_types=['text']) def echo_message(message): bot.reply_to(message, message.text) def main(req: func.HttpRequest) -> func.HttpResponse: # bot.set_webhook(url='https://0357c88a.ngrok.io/tlg') try: request_body_dict = req.get_json() update = telebot.types.Update.de_json(request_body_dict) bot.process_new_messages([update.message]) logging.info(req.get_json()) return func.HttpResponse(body='', status_code=200) except Exception as e: logging.error(e) |
Немного пояснений по коду. Всё, что прилетает в Azure Functions уходит в метод main()
в файле __init__.py
. Затем данные через триггеры уходят в req, которая описана в function.json — name. Это основная функция, которая запускается первой, в ней мы и запускаем процессинг обработки сообщений. Для req: func.HttpRequest мы принимаем http-запросы и делаем возврат в func.HttpResponse — 200. Здесь подробнее, что есть в пакете azure.functions.*.
Теперь надо объяснить телеграму, куда слать все сообщения. Для этого в pyTelegramBotAPI есть bot.set_webhook, но с ним лучше особо не играться, т.к. телега просто блокирует запросы от вас, поэтому самый простой вариант это использовать curl или просто открыть в браузере.
1 2 3 4 5 6 |
# установить webhook https://api.telegram.org/botВАШ_ТОКЕН/setWebhook?url=https://FUNCNAME.azurewebsites.net/ROUTE # проверить на какой endpoint телеграм будет слать все запросы https://api.telegram.org/botВАШ_ТОКЕН/getWebhookInfo |
После того, как вы установили webhook для телеграма, получили токен для бота, можно запустить локально. Помните, что ngrok при каждом перезапуске в бесплатной версии генерирует вам новый URL. Запускать ngrok надо с параметрами ngrok http 7071 — это означает, что весь трафик будет перенаправлен к вам локально на порт 7071, где крутится локальная функция. Например, webhook c ngrok будет такой — https://0357c88a.ngrok.io/tlg
Развернуть нашу функцию в Azure Functions можно разными способами (Azure CLI, портал Azure, Visual Studio), самое простое использовать VSCode c плагином Azure Functions.
Он создаст ресурсную группу, хранилище, Application Insights для мониторинга и сбора логов и саму функцию.


Вот этот URL+route из function.json и будет нашим webhook для telegram — https://mynewfunctelegram.azurewebsites.net/tlg
Если открыть Live Metrics Stream в Monitoring нашей функции, то будет видно прямой стрим данных и что происходит с ней. ;) То, что вы видите задержку на клиенте это на самом деле проблема между моим прокси и телеграмом, так что Azure Functions отвечает очень бодро.
Видно все, что происходит с функцией, что она получает и куда отправляет, доступен поиск через Log Analytics.


Какой можно сделать вывод из всего этого. Azure Functions клёво, я точно продолжну ставить эксперименты, попробую перетащить туда нашего бота из групп и посмотреть, как это всё будет работать в полупродакшене. Вам не надо думать о сертификатах, есть лайв мониторинг, легко интегрируется с blob storage или azure cosmosdb, быстрый деплой. Короче, есть куда копнуть еще. ;)
зыж ну и прайс, естественно — https://azure.microsoft.com/ru-ru/pricing/details/functions/

А если мой бот сохраняет в файлики данные? Файлики копеечные, но сохранять их надо, так как при каждом таком вызове Functions — придется считывать один из них (тот,что принадлежит конкретному юзеру) и записать туда новые данные (если надо).
Functions это могут? или там надо что-то еще подрубать?
фанкшены они stateless, но можно посмотреть в сторону — https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-blob?tabs=python#output
Привет, а этот способ еще работает? Сколько не пробую, локально все окей, но уже с Azure — никак