BackEndDevelopmentPHP7

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

Rating: 5.0/5. From 3 votes.
Please wait...

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

Содержание

It’s time to choose

Видеоверсия статьи доступна ниже, но я рекомендую сочетать её с текстом.

Что такое 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 ГБ-секунд в месяц бесплатно. Если первый параметр очевиден, то второй стоит описать подробнее. Гигабайт-секунды — это метрика объёма вычислений, которая представляет собой отношение общего времени выполнения к выделенной оперативной памяти. Итак, если у Вас есть 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

Напоследок давайте зарегистрируем нового бота в Telegram. Это делается через @BotFather командой /newbot. В итоге Вам вернётся токен для соединения с Вашим ботом. Его тоже нужно у себя сохранить.

BotFather

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

Теперь можем переходить непосредственно к коду.

Пишем код

Писать код мы будем с использованием 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, после чего бот становится доступен.

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

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

sls invoke local --docker -f myFunction

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

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

Логи

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

Итого

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

Дополнительные материалы по теме

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *