Это продолжение серий постов про «Бомжуем в 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 триггера.

http://masyan.ru/upload/2019/10/func1.mov

VScode создаст проект, все необходимые файлы и даст возможность запускать локально функции, как будто мы в облаке. ;) Открывайте Debug, запускаем и смотрим, что всё работает, видно, что реквесты летят и видно разные статусы.


https://masyan.ru/upload/2019/10/func2.mov

Для работы с telegram будем использовать простую либу — pytelegrambotapi. Идём в @botfather в телеграме, получаем токен, тут ничего нового. Плюс нам понадобится ngrok для локального тестирования. Если вы еще с ним не сталкивались, то он просто поднимает тоннель для тестирования приложений локально, может пробросить порт, выдаёт вам домен, поддерживает https и пишет логи. Очень крутая и бесплатная штука.

В нашем проекте надо поправить несколько файлов. В requirements.txt добавляем pyTelegramBotAPI и requests.

host.json

{
"version": "2.0",
"logging": {
"console": {
"isEnabled": "true"
}
},
"extensions": {
"http": {
"routePrefix": ""
}
}
}

По-дефолту, в Azure Functions URL формируется https://…/api/ЧтоТоЕщё, а если хотите убрать /api/, то надо оставить routePrefix пустым.

function.json

{
"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

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.jsonname. Это основная функция, которая запускается первой, в ней мы и запускаем процессинг обработки сообщений. Для req: func.HttpRequest мы принимаем http-запросы и делаем возврат в func.HttpResponse — 200. Здесь подробнее, что есть в пакете azure.functions.*.

Теперь надо объяснить телеграму, куда слать все сообщения. Для этого в pyTelegramBotAPI есть bot.set_webhook, но с ним лучше особо не играться, т.к. телега просто блокирует запросы от вас, поэтому самый простой вариант это использовать curl или просто открыть в браузере.

установить 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.

https://masyan.ru/upload/2019/10/func3.mov

Он создаст ресурсную группу, хранилище, Application Insights для мониторинга и сбора логов и саму функцию.

Вот этот URL+route из function.json и будет нашим webhook для telegram — https://mynewfunctelegram.azurewebsites.net/tlg

Если открыть Live Metrics Stream в Monitoring нашей функции, то будет видно прямой стрим данных и что происходит с ней. ;) То, что вы видите задержку на клиенте это на самом деле проблема между моим прокси и телеграмом, так что Azure Functions отвечает очень бодро.+

https://masyan.ru/upload/2019/10/func4.mov

Видно все, что происходит с функцией, что она получает и куда отправляет, доступен поиск через Log Analytics.

Какой можно сделать вывод из всего этого. Azure Functions клёво, я точно продолжну ставить эксперименты, попробую перетащить туда нашего бота из групп и посмотреть, как это всё будет работать в полупродакшене. Вам не надо думать о сертификатах, есть лайв мониторинг, легко интегрируется с blob storage или azure cosmosdb, быстрый деплой. Короче, есть куда копнуть еще. ;)

зыж ну и прайс, естественно — https://azure.microsoft.com/ru-ru/pricing/details/functions/