Serverless Telegram-бот на базе PHP и AWS Lambda | OTUS

Serverless Telegram-бот на базе PHP и AWS Lambda

PHP_Deep_30.12-5020-d6d1db.png

В сети есть огромное количество мануалов, посвящённых работе бессерверным окружениям от Amazon Webservices (и не только) и даже запуску «Hello, World!» на PHP внутри этих окружений. Но, решив самостоятельно собрать что-то дельное при помощи этих инструментов, я столкнулся с огромным количеством пробелов и подводных камней. Обойдя их, я в конце концов запустил простенького Telegram-бота, которого можно дорабатывать в любом направлении. Но давайте обо всём по порядку!

Что такое Serverless и AWS Lambda?

Если вы ещё не знакомы с концепцией так называемой «Бессерверной Архитектуры» или Serverless, у вас вполне может возникнуть совершенно логичный вопрос: «А зачем это всё нужно?». И начинать надо именно с понимания области применения данного подхода, чтобы потом не было мучительно больно.

Serverless — это относительно молодой метод запуска ваших скриптов в облаках, таких как AWS. Сейчас довольно популярны подходы Something As A Service (*aaS — например, IaaS, PaaS, SaaS), которые заключаются, грубо говоря, в предоставлении определённого уровня сервиса по клику мышки, будь то готовая среда выполнения для вашей логики или, например, знакомый многим Office 365. Вам ничего не надо устанавливать, кроме браузера, для того, чтобы начать работу. Так вот, Serverless является реализацией подхода FaaS (Function as a Service), заключающегося в предоставлении потребителю готовой платформы для разработки, запуска и управления некой функциональностью без необходимости самостоятельной её подготовки и настройки.

Вам не важно, где запускается скрипт, откуда провайдер возьмёт ресурсы и сколько он их возьмёт. Это означает, что приложение может легко масштабироваться до любых размеров в зависимости от нагрузки. Для PHP-скриптов это означает, что вам надо либо работать в парадигме Stateless, либо тщательно подумать о том, как будет храниться состояние (сессии). Благо, Serverless не накладывает ограничения на вызов необходимых хранилищ, будь то привычная многим MySQL база или какое-то более навороченное решение.

Важной особенностью Serverless является короткий цикл работы скрипта (в зависимости от платформы — до нескольких часов). Если мы говорим про AWS, то их таймаут составляет 15 минут, поэтому аналитические расчёты или огромный map-reduce на час тут не прокатят. Но вот запустить того же telegram-бота, который должен быстро отвечать пользователю — это довольно штатная задача для Serverless-приложений. Это означает, что на них нацелены те команды и разработчики, которые хотят быстро запустить небольшое приложение. И это же говорит о том, что стоимость данных услуг очень и очень низкая при понимании вышеозначенных ограничений.

Мощности Lambda являются настраиваемыми, но занятный факт в том, что у AWS мощности CPU для Lambda недоступны для настройки и увеличиваются автоматически прямо пропорционально объёму выделенной RAM. Таким образом, увеличивая объём RAM, вы буквально ускоряете приложение по всем фронтам.

AWS Lambda работает по принципу «Триггер — Выполнение — Последствия». Триггер — это событие, которое побуждает Lambda к выполнению. Последствия — это то, что порождает Lambda — запись в БД, логи, вызов других Lambda. При первом вызове для выполнения Lambda на стороне AWS генерируется микро-виртуальная машина, которая будет жить некоторое время на случай последующих вызовов. На эту машину загружается код из S3 хранилища, готовится среда выполнения и происходит вызов функции.

Цены на сервис

На момент написания статьи AWS предоставляет 1 миллион запросов и 400 000 ГБ-секунд в месяц бесплатно. Если первый параметр очевиден, то второй стоит описать подробнее. Гигабайт-секунды — это метрика объёма вычислений, которая представляет собой отношение общего времени выполнения к выделенной оперативной памяти. Итак, если у dас есть 1,5 миллиона запросов по 1 секунде каждый, а для скрипта выделено 256 мегабайт RAM, то суммарный объём вычислений будет равен:

S = 1 500 000 * 1 * 256 / 1024 = 375 000 ГБ-с

Как видите, этот объём укладывается в лимит бесплатного сервиса, но количество запросов превышает 1 миллион и тарифицируется (по ценам на момент написания статьи) по 20 центов за миллион запросов. Это означает, что 1,5 миллиона запросов в месяц к своему скрипту вы заплатите 10 центов.

Но не торопитесь бежать и открывать аккаунт в AWS. Помимо этого тарифицируются смежные сервисы: — API Gateway — самый простой, но самый дорогой способ вызова Lambda в AWS; — Elastic LoadBalancing (ELB) — на мой взгляд, оптимальный способ вызова; — Route 53 — для работы бота, например, понадобится доменное имя. За него надо платить; — SSL-сертификат — также требование Telegram API; — S3 — аам потребуется хранилище для того, чтобы деплоить приложение.

Тем не менее, если вы не используете API Gateway, все сервисы суммарно тоже не очень дорогие. ELB при работе целый месяц в режиме 24/7 при 25 новых соединениях в секунду обойдётся в 22.5 доллара в месяц. Это уже история для довольно плотной загрузки скрипта. При работе в режиме 1 запрос в минуту сумма будет значительно меньше. Сервис DNZ будет стоить 50 центов за месяц использования одной зоны (разумеется, выгоднее использовать DNS-зону не только для бота). 100 000 запросов обойдутся в 0.04-0.06 доллара в месяц. Сертификат для 1 зоны будет стоить 0.75 долларов в месяц. S3 стоит 0.0245 доллара за 1 ГБ, которого вполне хватит, если будете деплоить логику несколько раз в неделю.

Если вы уже используете облачные решения для своих приложений, то ценообразование для вас будет выгодно за счёт того, что цены делятся между сервисами, а не уходят только на обслуживание Lambda.

Тонкости организации работы

Теперь поговорим о практике. Как я уже написал выше, самым простым, но самым дорогим (в несколько раз дороже ELB) способом вызова Lambda в AWS является API Gateway. По интересному стечению обстоятельств, именно этот тип вызова указывается во всех мануалах по настройке окружения. Ценообразование вполне логично — чем меньше ты делаешь своими руками, тем больше платишь. Мы же будем рассматривать настройку при помощи ELB, чтобы экономить деньги.

Сам Amazon в своих мануалах предлагает деплоить код, упаковывая его в zip-архивы и загружая на их мощности через AWS API или через интерфейс. В воздухе запахло 2000-ными. Конечно же, это не подход к работе с доставкой кода в современных реалиях, поэтому мы будем использовать принятый многими как стандарт Serverless framework, который разработан на Node.js и позволяет управлять приложениями на базе AWS Lambda.

Ещё одной особенностью является то, что AWS Lambda не поддерживает из коробки вызов PHP-скриптов. А это означает, что нам придётся создавать среду исполнения самостоятельно. Благо сейчас на рынке появилась PHP-библиотека Bref, интегрированная с Serverless, решающая эту задачу. Тем не менее, вы можете самостоятельно собрать среду исполнения и подружить её с Lambda API. Но дело это неблагодарное, так как проблема уже решена изящным и приемлемым способом.

Настраиваем окружение и AWS

AWS CLI

Начинать стоит с создания аккаунта в AWS и установки AWS CLI. Консольная оболочка от AWS основывается на Python версий 2.7+ или 3.4+. AWS рекомендуют 3 версию языка, поэтому спорить с этим решением мы не будем. Примеры приведу для Ubuntu, но вы смело можете интерпретировать их для своих Linux-дистрибутивов.

sudo apt-get -y install python3-pip

Далее установим непосредственно утилиту AWS CLI:

pip3 install awscli --upgrade --user

Проверку установки можно сделать при помощи:

aws --version

После этого нужно будет подключить aws cli к вашему аккаунту. Конечно, можно использовать и ваши логин и пароль, но лучше создать отдельного пользователя через AWS IAM и установить ему только необходимые права доступа. Сама конфигурация вызывается просто:

aws configure

На этом шаге вам нужны будут AWS Access Key и AWS Secret. Оба параметра можно найти в ASW IAM, зайдя на страницу нужного вам пользователя и выбрав вкладку Security credentials. На ней будет кнопка «Create access key», которая позволит сгенерировать ключи доступа. Зафиксируйте их у себя.

AWS IAM: Снимок_экрана_от_2019_10_25_14_04_09_1024x427_1-20219-dc5077.png Напоследок давайте зарегистрируем нового бота в Telegram. Это делается через @BotFather командой /newbot. В итоге вам вернётся токен для соединения с вашим ботом. Его тоже нужно у себя сохранить.

BotFather: Снимок_экрана_от_2019_10_25_16_14_32_1-20219-a513a0.png

Serverless Framework

Для установки вам также потребуется аккаунт на https://serverless.com/. Версия для разработки бесплатна, и её функционала нам хватит за глаза.

После регистрации нужно установить утилиту serverless у себя на рабочей станции. Я уже отметил, что нам потребуется Node.js (нужна версия 6 и выше).

sudo apt-get -y install nodejs

Для того, чтобы он корректно запускался в нашей среде, нужно также выполнить рекомендованные шаги:

mkdir ~/.npm-global
export PATH=~/.npm-global/bin:$PATH
source ~/.profile
npm config set prefix ~/.npm-global

Также добавьте

~/.npm-global/bin:$PATH

в файл /etc/environment.

После этого можем ставить Serverless:

npm install -g serverless

AWS

Теперь перейдём в интерфейс AWS и добавим доменное имя. Создайте в AWS Route 53 зону, DNS-запись и SSL-сертификат для неё.

Также на понадобится ELB. Его мы создаём в сервисе EC2 -> Load Balancers. При создании ELB пройдите все шаги мастера, указав при выполнении созданный сертификат.

Можно создать балансировщик и через AWS CLI примерно такой командой:

aws elb create-load-balancer --load-balancer-name my-load-balancer --listeners "Protocol=HTTP,LoadBalancerPort=80,InstanceProtocol=HTTP,InstancePort=80" "Protocol=HTTPS,LoadBalancerPort=443,InstanceProtocol=HTTP,InstancePort=80,SSLCertificateId=arn:aws:iam::123456789012:server-certificate/my-server-cert" --subnets subnet-15aaab61 --security-groups sg-a61988c3

Он потребуется нам после первого деплоя. Нам нужно направить на него запросы к нашему домену. Для этого в настройках DNS-записи в поле Alias target начните вводить название созданного ELB. Выберите его в выпадающем списке и сохраните запись.

DNS ELB: Снимок_экрана_от_2019_10_25_16_05_58_1024x549_1-20219-0763c6.png Теперь можем переходить непосредственно к коду.

Пишем код

Писать код мы будем с использованием Bref. Эта библиотека ставится при помощи composer, так что наш код будет совместим с любым фреймворком. Кстати говоря, создатели Bref уже описали процесс использования библиотеки с Symfony и Laravel. Но мы будем работать на «голом» PHP, чтобы лучше понять суть. Начнём с зависимостей.

{
    "require": {
        "php": ">=7.2",
        "bref/bref": "^0.5.9",
        "telegram-bot/api": "*"
    },
    "autoload": {
        "psr-4": {
            "App\": "src/"
        }
    }
}

Как видите, будем работать на PHP 7.2 и выше, а для работы с Telegram будем использовать вот эту оболочку https://github.com/TelegramBot/Api к API. Сам код будет располагаться в директории src.

Бессерверная среда собирается через консольный диалог. Нам нужно HTTP-приложение. С точки зрения Lambda это означает, что вызов скриптов будет происходить аналогично тому, как это делает Nginx. Интерпретация будет происходить силами PHP-FPM. В обычном случае это больше похоже на обычный консольный вызов скрипта. Это важно, так как без учёта этой особенности скрипты через HTTP вызывать не получится. Выполним:

vendor/bin/bref init

В диалоге нам нужно будет выбрать пункт «HTTP application» и не забыть указать регион. Ваше приложение должно работать в том же регионе, в котором работает ваш балансировщик.

После инициализации у вас появятся два новых файла: — serverless.yml — файл настройки деплоя; — index.php — вызываемый файл.

Сразу стоит добавить в .gitignore папку .serverless, которая появится после первой попытки деплоя.

Коль скоро у нас веб-приложение, то скинем сразу же index.php в папку public, и сразу переключимся на serverless.yml. В нашей реализации он может выглядеть вот так:

# имя lambda-приложения
service: app

# описание провайдера услуг
provider:
    name: aws
    # указываем регион балансировщика!
    region: eu-central-1
    # среда выполнения нестандартная
    runtime: provided
    # вообще, для bref рекомендуют 1024. Но для простого скрипта столько и не надо
    memoryLimit: 256
    # указываем окружение
    stage: dev

    # глобальные переменные окружения
    environment:
        BOT_TOKEN: ${ssm:/app/bot-token}

# подключаем bref
plugins:
    - ./vendor/bref/bref

# описание Lambda-функций
functions:
    # наша функция в итоге будет называться php-api-dev
    # service-function-stage
    api:
        handler: public/index.php
        description: ''
        # in seconds (API Gateway has a timeout of 29 seconds)
        timeout: 28
        layers:
            - ${bref:layer.php-73-fpm}
        # возможные события вызова для API Gateway
        events:
            -   http: 'ANY /'
            -   http: 'ANY /{proxy+}'
        # локальные переменные окружения
        environment:
            MY_VARIABLE: ${ssm:/app/my_variable}

Разберём неочевидные строчки. Больше всего нам нужны переменные окружения. Ведь мы не хотим хардкодить подключения к БД, внешним API и прочему. А если уж мы подключаемся к Telegram, то у нас будет свой токен, полученный от BotFather. И хранить его в serverless.yml не рекомендуется, поэтому мы отправим его в ssm-хранилище AWS:

aws ssm put-parameter --region eu-central-1 --name '/app/my_variable' --type String --value 'ТОКЕН_БОТА_ОТ_BOTFATHER'

Именно к нему мы и обращаемся в конфигурации.

Данные переменные доступны как переменные окружения, и получить к ним доступ в PHP можно при помощи функции getenv. В нашем примере я сохраняю токен бота в глобальной области видимости для простоты. Но можно перенести его в область видимости отдельно взятой функции. Сам вызов от этого не изменится.

Теперь создадим простой класс BotApp, который будет отвечать генерацию ответа для нашего бота. Он будет реагировать на команды. Создатели Telegram рекомендуют для всех ботов добавлять поддержку команд /start и /help. Мы для разнообразия добавим ещё одну команду. Сам класс очень простой и позволяет в index.php реализовать Front Controller, не нагружая сам файл вызова кодом. Для более сложной логики архитектуру, разумеется, нужно развивать и усложнять.

<?php

namespace App;

use TelegramBot\Api\Client;
use Telegram\Bot\Objects\Update;

class BotApp
{
    function run(): void{
        $token = getenv('BOT_TOKEN');

        $bot = new Client($token);
        // команда для start
        $bot->command('start', function ($message) use ($bot) {
            $answer = 'Добро пожаловать!';
            $bot->sendMessage($message->getChat()->getId(), $answer);
        });

        // команда для помощи
        $bot->command('help', function ($message) use ($bot) {
            $answer = 'Команды:
            /help - вывод справки';
            $bot->sendMessage($message->getChat()->getId(), $answer);
        });

        // тестовая команда
        $bot->command('hello', function ($message) use ($bot) {
            $answer = 'Да-да, я - бот, работающий в Serverless окружении';
            $bot->sendMessage($message->getChat()->getId(), $answer);
        });

        $bot->run();
    }
}

А вот листинг index.php

<?php

require_once('../vendor/autoload.php');

use App\BotApp;

try{
    $botApp = new BotApp();
    $botApp->run();
}
catch (Exception $e){
    echo $e->getMessage();
    print_r($e->getTrace(), 1);
}

Как ни странно, всё готово к тому, чтобы уехать на Production. Сделаем это, выполнив команду в папке, где лежит serverless.yml:

sls deploy

В штатном режиме serverless упакует все файлы в zip архивы, создаст S3-bucket, куда положит их, после чего создаст или обновит AWS Application, привязанный к Lambda и в отдельный слой положит код и среду выполнения. При первом запуске у вас создастся API Gateway (я оставил его, чтобы можно было просто потестировать вызовы). Но потом его лучше удалить. Надо будет настроить вызов Lambda через ELB. Для этого в окне управления функцией выберите «Add trigger» и в появившемся выпадающем списке — Application Load Balancer. Укажите созданный ранее ELB, задайте соединение через HTTPS, Host оставьте пустым, а в Path укажите путь, который будет вызывать Lambda (например /lambda/mytgbot). После этой настройки ваша Lambda станет доступна по URL с указанием заданного вами пути.

Теперь мы можем зарегистрировать ответную часть нашего бота в Telegram, чтобы мессенджер знал, откуда брать сообщения. Для этого в браузере вызовите вот такой URL, подставив в него свои параметры

https://api.telegram.org/botТОКЕН_БОТА/setWebhook?url=https://my-elb-host.com/lambda/mytgbot

API должен вернуть ответ OK, после чего бот становится доступен. Снимок_экрана_от_2019_10_25_12_57_15_1-20219-b4e7f1.png

Тестирование на локали

Стоит отметить, что бот можно потестировать и до деплоя. Serverless-фреймворк поддерживает запуск на локали, используя для этого Docker-контейнеры. Вызов производится вот такой командой:

sls invoke local --docker -f myFunction

Не забудьте, что мы использовали переменные окружения, поэтому при вызове их тоже надо задавать в формате:

sls invoke local --docker -f myFunction --env VAR1=val1

Логи

По умолчанию AWS будет логировать вывод вызова в CloudWatch — он доступен в панели Monitoring соответствующей Lambda-функции. Здесь можно будет почитать трейсы вызовов в случае отвала на стороне PHP. Но также можно подключить и расширенные сервисы мониторинга, которые обойдутся вам в дополнительные несколько центов в месяц.

Итого

Мы научились получать быстрое гибкое скалируемое и относительно дешёвое решение, позволяющее нам собирать и запускать простые решения, которые зачастую не нужны нам постоянно и не требует жёстко заданной среды выполнения. Преимущества Lambda не всегда выигрывают перед стандартными виртуальными машинами и контейнерами, но есть случаи, когда Serverless-приложение помогает «выстрелить» быстро и эффективно. Пример нашего бота как раз демонстрирует такой подход.

Дополнительные материалы по теме: 1) теория Lambda, 2) документация Bref, 3) анатомия Lambda, 4) Serverless Framework.

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться
Популярное
Сегодня тут пусто