"Whenever you receive a webhook update, you have two options"
Из Telegram Bot Api Faq
Привет, Хабр!
Долгое время концепция serverless (а если говорить точнее — её реализация в виде сервиса AWS Lambda) была для меня относительно понятной, но очень абстрактной идеей. Она часто звучала в Radio-T, обсуждениях на реддите, но никак не входила в мою жизнь. Рабочие проекты живут не в облаке, а домашние — зачем? Виртуалки дешевеют, Docker освоен, и всё отлично работает.
Но презентация Yandex Cloud Functions, и в особенности озвученные цены на этот сервис, дали новую пищу для размышлений.
TL;DR — дождливым пятничным вечером мы напишем простенького Telegram бота на javascript, который сможет отвечать на запросы простыми сообщениями. Если это ваш домашний проект — его использование почти наверняка обойдётся вам значительно дешевле самых бюджетных VPS.
Поехали.
Что такое Serverless в самом бытовом понимании?
Не буду очень глубоко уходить в дебри, на Хабре регулярно появляются обзорные статьи на эту тему. Это возможность разместить в облаке функцию, на каком-то из поддерживаемых платформой языков программирования, задать условие её срабатывания — и всё. Когда случится триггер — поднимется виртуальное окружение, в нем отработает функция, и выключится. Вместе с окружением.
?В чем преимущества такого подхода?
Безопасность
Вы получаете безопасное изолированное окружение с последней версией компилятора / интерпретатора.
Вместо того, чтобы следить за обновлением пакетов на настоящей ОС в Виртуальной машине, заниматься настройкой политик безопасности и файрволла — вы загружаете программу на сервер, и она работает.
Стабильность и отказоустойчивость
Вместо того чтобы конфигурировать pm2, настраивать политику перезагрузок, следить за утечкой памяти и постигать нюансы деплоя — да, вы просто загружаете программу на сервер, а всё остальное берёт на себя провайдер услуги.
Цена, особенно в условиях домашних малонагруженных проектов
При тарификации учитывается объём памяти, резервируемый под функцию за время её исполнения и количество вызовов. Согласно документации 10 000 000 запусков функции, работающей 800ms с ограничением по памяти 512мб будут стоить 3 900?.
Что это означает для меня? Мой типичный петпроджект — бот отвечающий на вопросы по расписанию для широко известного в узких кругах мероприятия. Запускать его нужно раз в год, на протяжении нескольких дней. В прошлом году он ответил на 1000 запросов от участников, ему более чем хватит 128мб, время выполнения функции 300ms. Такой сценарий использования будет стоить 0.046?.
Да, 4.6 копеек. Плюс я не буду тратить время на настройки, что ещё приятней. Никаких правил для pm2, никакой актуализации Dockerfile или окружения, и вишенкой на торте — SLA 99,9.
Из пока нерешённых задач (но, полагаю, это вопрос времени) — привязка внешних доменов, а так же тонкая настройка http методов, которые служат триггером функции. Сейчас http триггер срабатывает на любой из DELETE, GET, HEAD, OPTIONS, PATCH, POST или PUT запросов на авто-генерируемую точку входа типа https://functions.yandexcloud.net/xxxxxxxxxxxxxxxx.
Из хороших новостей — это полноценный https, который отвечает всем требованиям Telegram для работы с api через webHooks. Но у AWS Lambda есть дополнения в виде API Gateway, да и сама настройка триггера шире, если вам это нужно.
Очевидное ограничение самого serverless подхода, не зависимо от платформы — пользоваться приходится ровно тем, что дают. Написать код на неподдерживаемых языках программирования или использовать нестандартные параметры компилятора/интерпретатора вы не сможете. Также возможны дополнительные ограничения призванные защитить всех участников процесса разработки.
Как создать .js файл, чтобы работать в Яндекс.Облаке?
Краткий гайд через веб интерфейс:
- создаём Функцию
- создаём в веб интерфейсе файл с любым именем и расширением js
- выбираем интерпретатор — nodejs10 или nodejs12
- в файле пишем функцию с одним параметром в exports.myFunction (ну, в произвольное поле в exports)
- указываем таймаут работы функции, оперативную память (128МБ-1024МБ с шагом в 128МБ), точку входа (имяфайла.myFunction)
- делаем функцию публичной
Функция написанная в файле может:
Получить данные http запроса через входящий параметр:
функция не получает request в чистом виде, и конечно же не управляет ходом запроса — она получает в своём единственном параметре объект с информацией о запросе:
{
"httpMethod": "<название HTTP метода>",
"headers": "<словарь со строковыми значениями HTTP-заголовков>",
"multiValueHeaders": "<словарь со списками значений HTTP-заголовков>",
"queryStringParameters": "<словарь queryString-параметров>",
"multiValueQueryStringParameters": "<словарь списков значений queryString-параметров>",
"requestContext": "<словарь с контекстом запроса>",
"body": "<содержимое запроса>",
"isBase64Encoded": <true или false>
}
Ответить на http запрос
согласно документации:
{
"statusCode": <HTTP код ответа>,
"headers": "<словарь со строковыми значениями HTTP-заголовков>",
"multiValueHeaders": "<словарь со списками значений HTTP-заголовков>",
"body": "<содержимое ответа>",
"isBase64Encoded": <true или false>
}
Итак, что-нибудь пятничное, бесполезное
Сначала, посмотрим на то что уже написано до нас — реализаций таких ботов для AWS Lambda вагон и маленькая тележка.
У них есть одна проблема — для того чтобы не изобретать велосипед, и обеспечить привычный интерфейс, все эти реализации при получении запроса инициируют post до сервера api telegram. Но можно поступить проще.
Как можно было заметить на КДПВ, и цитате в начале поста — при работе через webHook, telegram слушает ответ на своё update сообщение, чтобы понять, был ли он обработан нашим ботом. Более того, он готов принять сообщение в рамках этого же ответа.
Согласно документации в ответе должна содержаться лишь одна функция (проверил sendMessage и sendPhoto). Для многих проектов этого будет достаточно.
Соблюдём традиции, и передадим привет Хабровчанам:
exports.input = function (data){
let body = JSON.parse(data.body);
let answer = {
"method":"sendMessage",
"chat_id": body.message.chat.id,
"reply_to_message_id" : body.message.message_id,
"text" : "Привет, Habr!"
};
return {
"statusCode": 200,
"headers": {
'Content-Type': 'application/json'
},
"body": JSON.stringify(answer),
"isBase64Encoded": false
}
}
Выставим настройки в минимум:
И сообщим в Telegram, что будем использовать webHook:
curl -F "url=https://functions.yandexcloud.net/{secret_function_id}" https://api.telegram.org/bot{secret_bot_key}/setWebhook
Всё. Бот работает.
С ним можно пообщаться: @YandexServerlessBot
Подводя итог — в некоторых случаях serverless это крайне дёшево, удобно, и экономит кучу времени, а любую документацию надо читать внимательно: тогда она может приятно удивить.
Если вам стало интересно — добро пожаловать в документацию по Yandex Cloud Functions, там много интересного, от интеграции с другими сервисами облака до дебага, графиков нагрузки и т.д.
Видео с конференции также доступно на YouTube.
UPD: Как показало дальнейшее исследование (спасибо IRT за наводку) — сервера tg доступны и без таких ухищрений, так что можно смело использовать традиционные api запросы.
Комментарии (43)
IRT
05.10.2019 08:44+1все эти реализации при получении запроса инициируют post до сервера api telegram, что в условиях крупного Российского хостера невыполнимо
Во-первых, провайдеры хостинга, датацентры и так далее не являются операторами связи. Они ничего не блокируют из списка РКН в своих сетях. В основном. Во всяком случае, если они что-то блокируют, это исключительно их инициатива, по закону они не обязаны. У меня есть прокси Telegram, поднятые на российских VPS, до сих пор работают.
Во-вторых, если webhook с серверов Telegram приходит, то очевидно, что и API так же доступно. Я еще не видел блокировок только в одну сторону.
Но за описанную возможность сразу отправить ответ, не дергая API телеги отдельным запросом спасибо, не знал.NiPh Автор
05.10.2019 09:57Проверил, вы правы, добавил UPD в конце статьи. Мои хостеры в своё время блокировали telegram.org и всё что рядом, так что привык воспринимать это как данность.
Anton23
05.10.2019 10:32Не боитесь «попасть на деньги» от Хабра-эффекта?)
NiPh Автор
05.10.2019 11:03
Пока нет ) Предполагаю что даже до стоимости проезда в метро не дойдёт.
akdes
05.10.2019 13:35+1Challenge accepted :D (нет)
Но думаю, могут найтись, "доброжелатели"..
При том, что можно написать просто бота, который будет дергать Вашего нное количество раз.
За статью спасибо.tmin10
05.10.2019 14:04Можно же добпвить простую проверку, чтоюы не отвечать юзеру более N раз в час. Тонда злоумышленнику нужно регистрировать много аккаунтов в телеграме уже.
akdes
05.10.2019 14:10Проверка будет в скрипте, соответственно вызываться он так или иначе будет.
Blaine_Mono
05.10.2019 16:34Проверка будет занимать меньше времени чем работа самого скрипта.
akdes
05.10.2019 16:44Т.е. Вы считаете, что регистрация айди пользователя и времени/частоты его запросов, а также последующая проверка, будут работать быстрее чем ответ "Привет Хабр"?
Blaine_Mono
05.10.2019 22:30Вот в этом случае уже действительно регистрация id не осмысленна.
А в случае если бот имеет какую-то полезную нагрузку — регистрация id пользователя и подсчет его активности уже будет иметь смысл.
tmin10
05.10.2019 18:54Точно. Тогда нужен некий порог, после которого лямбда сворачивается и поднимается уже виртуалка для оптимизпции расходов.
skrimafonolog
05.10.2019 16:16и вишенкой на торте — SLA 99,9
Не такая уж это и вишенка.
Допустим простой 8ч 45м 57с в год.
Полный рабочий день включая обеденный перерыв, другими словами.
akdes
05.10.2019 16:45Вы можете гарантировать больше?
skrimafonolog
06.10.2019 08:13+1Вы можете гарантировать больше?
Несведующему человеку указанная 99,9% может показаться высокой, но это не так.
К примеру, при определенном масштабе деятельности, когда можно выделить круглосуточных админов с их дублированием (разумеется не под один этот сервер). И даже дешевле, без облака, но ручками — да. При нормальной организации репликации — сменить хостера при сбое дело нескольких минут.
Далее, для достижения уровня доступности 99,95% вам понадобится просто построить кластер active-passive.
Если вы хотите перешагнуть за 99,982% (уровень доступности в дата-центрах Tier III), вам нужно строить систему, распределенную по нескольким ЦОД.
Еще раз:
Цифры SLA намного более высокой доступности, чем 99,9% — вполне реальны в нашем мире.
Tiendil
06.10.2019 09:14При нормальной организации репликации
:-D
сменить хостера при сбое дело нескольких минут
:-DD
просто построить кластер active-passive
:-DDD
Это запредельный уровень допущений. То есть наверняка есть люди, которые могут это сделать и есть конторы, которые могут дать на организацию всего этого время. Но это мизерная часть от всех людей и всех контор.gecube
06.10.2019 10:06О чем мы спорим? Яндекс заявляет 99.9. ну, ок. Хорошо.
В принципе, с этим можно жить.
А дальше — как я пишу ниже — дьявол в деталях. Я просто внимталеьно смотрел договора (речь не про Яндекс как таковой — возможно, что там все ок ) И все там не так просто как "мы гарантируем". Ну, положим не выполнил поставщик / провайдер / облако свои гарантии — ну, заплатит он вам штраф. Или даст бесплатный бонус. И ЧТО? Репутацию и клиентов это вам не вернёт.
gecube
06.10.2019 10:04На самом деле даже 99.9 никто не гарантирует. Если подумать.
Читайте договор мелким шрифтом. И скорее всего там будет, что доступность платформы (т.е. веб-панкели и АПИ), а сами сервисы… Ну, не летит трафик в вашу лямбду. И что? Это у вас провайдер фиговый — к нему и идите )
Catsys2
05.10.2019 19:43Но база то все равно должна где то лежать, что бы к ней дотянуться из этой функции? Это уже контейнер, проще на одном контейнере все поднять, если он все равно нужен. Тем более для пет-проекта
NiPh Автор
05.10.2019 21:49Это уже зависит от архитектуры приложения. Одному из моих ботов хватит json файлика лежащего рядом, который будет раз в пол года меняться. Другому — папки с иногда добавляющимися файлами. Третий ходит по внешним api.
Если понадобится база — я скорее буду смотреть на cloud managed решения, тот же MySQL в Яндексе будет стоить 508? в месяц, и обслуживать несколько проектов. С бэкапами, администрированием и тем-же SLA. В Амазоне, кстати, примерно столько же RDS стоит.
Но да, безусловно облако не панацея и есть множество случаев когда или надо переделывать под него архитектуру чтобы не разориться, или сразу завести всё на бюджетной vds за 1,5$ в месяц.
RStarun
05.10.2019 21:27Странно что бесплатного лимита нет. У меня давно крутится пара домашних ботов в Google App Engine, и вроде совсем бесплатно.
Минус в том что решение в облаках раз в пару лет обычно нужно чуть чуть переделывать. Потому что облачное окружение меняется. Могут исключить встроенную библиотеку например.NiPh Автор
05.10.2019 21:29Лимита нет, но есть бесплатный тестовый период cloud.yandex.ru/docs/free-trial
Shersh
05.10.2019 23:08А еще можете посмотреть на azure functions. Там есть большое кол-во триггеров и бесплатные первые 4 млн вызовов в месяц)
gecube
06.10.2019 00:48Я так понимаю, что дьявол в деталях. И хоть сами вызовы ничего не стоят, но их роутинг через api gateway или что там у ажура может влететь в копеечку. Как прокомментируете ?
Shersh
06.10.2019 09:00+1В отличии от aws там api routing бесплатен. :) в копеечку вам может влететь работа с бд. Или очень долгое выполнение функции.
gecube
06.10.2019 01:00Вы получаете безопасное изолированное окружение с последней версией компилятора / интерпретатора.
Вместо того, чтобы следить за обновлением пакетов на настоящей ОС в Виртуальной машине, заниматься настройкой политик безопасности и файрволла — вы загружаете программу на сервер, и она работает.Звучит очень двояко. А если я не хочу использовать последнюю версию? А если я вообще питонщик и у меня завязка на конкретные версии модулей?
И фраза, что загружаешь и оно работает слишком… Пафосная. Скажем так. Есть два подхода к написанию лямбд. Первый — поднять локальное окружение или эмулятор лямбды. Среды. Амазон, кстати, такой предоставляет. И в нем полностью написать и прогнать свой код. Ну, либо можете наживую отлаживать в облаке (не очень хорошо). Ну, либо — писать будто никакой лямбды нет, потом просто обернуть свой код и запушить в облако. Но могут быть сюрпризы.
Ещё лямбда (=серверлесс) имеет кучу минусов. Это и мониторинг. И отладка. И в целом перенос сложности с одного уровня проекта на другой. Отдельный вопрос — как будут сосуществовать разные версии кода в одном облаке. Бр. В общем — когда нужно написать что-то маленькое, что-то быстрое — serverless прекрасен, но как только нужно написать полноценный сервис, то стоит подумать и об экономике (в промышленных масштабах лямбда дорога), об ограничениях (лимиты памяти, времени выполнения) и удобстве поддержки
NiPh Автор
06.10.2019 11:06+1Очевидное ограничение самого serverless подхода, не зависимо от платформы — пользоваться приходится ровно тем, что дают. Написать код на неподдерживаемых языках программирования или использовать нестандартные параметры компилятора/интерпретатора вы не сможете. Также возможны дополнительные ограничения призванные защитить всех участников процесса разработки.
Да, всё так. Возможно перебрал пафоса с «последней версией», выбирается конкретный мажорный релиз, с nodejs10 на nodejs12 вас автоматически без предупреждений не переведут.
Aspire89
06.10.2019 14:45А подскажите как отправить «multipart/form-data», хочу отправить sendPhoto, но ничего не выходит
import json import base64 import urllib.request def handler(event, context): img = urllib.request.urlopen("https://habr.com/images/habr.png").read() parsed_string = json.loads(event["body"]) chat_id = parsed_string['message']['chat']['id'] answerM = { "method": "sendMessage", "chat_id": chat_id, "text": "привет" } answerP = { "method": "sendPhoto", "chat_id": chat_id #"photo": base64.b64encode(img).decode() } return { 'statusCode': 200, 'headers': {"Content-Type": "multipart/form-data"}, 'body': json.dumps(answerP), 'isBased64Encoded': 'true', 'files': {'photo': base64.b64encode(img).decode()} }
NiPh Автор
06.10.2019 15:11Я не вижу в документации возможности отдельно прикладывать файлы в поле files, думаю что надо самостоятельно объявить delimiter в заголовке и подготовить данные с ним в body
Aspire89
06.10.2019 18:11Добавил Content-Disposition
import json import base64 import urllib.request def handler(event, context): img = urllib.request.urlopen("https://habr.com/images/habr.png").read() parsed_string = json.loads(event["body"]) chat_id = parsed_string['message']['chat']['id'] boundary = 'Asrf456BGe4h' parts = [] parts.append('--' + boundary) parts.append('Content-Disposition: form-data; name="method"') parts.append('') parts.append('sendPhoto') parts.append('--' + boundary) parts.append('Content-Disposition: form-data; name="chat_id"') parts.append('') parts.append(str(chat_id)) parts.append('--' + boundary) parts.append('Content-Disposition: form-data; name="photo"; filename="%s"' % 'img.jpg') parts.append('Content-Type: image/jpeg') parts.append('') parts.append(base64.b64encode(img).decode()) parts.append('--' + boundary + '--') parts.append('') body = '\r\n'.join(parts) return { 'statusCode': 200, 'headers': { 'content-type': 'multipart/form-data; boundary=' + boundary }, 'body': body, 'isBased64Encoded': 'true' }
И добился формирования следующего ответа, но не понимаю, что из этого не нравится телеграму:
HTTP/1.1 200 OK Server: nginx Date: Sun, 06 Oct 2019 14:26:32 GMT Content-Type: multipart/form-data; boundary=Asrf456BGe4h Transfer-Encoding: chunked Connection: keep-alive X-Content-Type-Options: nosniff X-Function-Id: ... X-Function-Version-Id: ... X-Request-Id: ... Strict-Transport-Security: max-age=31536000; includeSubdomains; preload X-Frame-Options: SAMEORIGIN --Asrf456BGe4h Content-Disposition: form-data; name="method" sendPhoto --Asrf456BGe4h Content-Disposition: form-data; name="chat_id" 1111111 --Asrf456BGe4h Content-Disposition: form-data; name="photo"; filename="img.jpg" Content-Type: image/jpeg iVBORw0KGgoAAAANSUhEUgAABLAAAAJ2CAYAAABPQHtcAAAABGdBTUEAALGPC/ .../A0wmf2FFW4KcAAAAAElFTkSuQmCC --Asrf456BGe4h--
NiPh Автор
06.10.2019 23:53Явного упоминания такой готовности в доках уже телеграма нет, я пробовал sendPhoto в таком виде:
exports.input = function (data){ let body = JSON.parse(data.body); let answer = { "method":"sendPhoto", "photo": "https://habrastorage.org/webt/hr/f7/nl/hrf7nll2xn4c9leojqsc95uv7ji.png", "chat_id": body.message.chat.id, "reply_to_message_id" : body.message.message_id, "text" : "Привет, Habr!" }; return { "statusCode": 200, "headers": { 'Content-Type': 'application/json' }, "body": JSON.stringify(answer), "isBase64Encoded": false } }
Groosha
07.10.2019 12:19Как подсказали ниже/выше, для небольших картинок (меньше 5 Мб) можно указать просто URL.
Если набор изображений более-менее стабильный и отбирается вручную, можно заранее отправить их своему боту в Telegram, через метод getUpdates посмотреть на fileID этих картинок и дальше вызывать sendPhoto с этими fileID, тогда отправка будет вообще мгновенной, ведь файл уже лежит на серверах Telegram.Aspire89
07.10.2019 12:29Да, такой вариант работает, но не в моем случае.
У меня URL картинки всегда один, а содержимое отличается (веб камера) и телеграм запоминает первое содержимое и дальше всегда его отдает.Groosha
07.10.2019 12:30+1Может быть, костыль, но что если добавлять к URL картинки какой-нибудь невзрачный параметр? Типа https://example.com/pic.jpg?yourvalue=abcxyz
Groosha
Не совсем понимаю, почему такое ограничение на вызов лишь одного sendMessage. Разве нельзя вызвать sendPhoto, к примеру?
NiPh Автор
Да, вы правы, призывал читать документацию, и сам не дочитал.
Можно — @YandexServerless2Bot