Безсерверні веб-застосунки на Python з використанням Lambda і Flask
Це перший із серії матеріалів, присвячених розробці безсерверних веб-застосунків на Python.
З погляду розробника, безсерверні застосунки — чудове рішення: не треба підтримувати інфраструктуру; про масштабування у разі зростання кількості запитів теж можна не думати — воно відбувається автоматично; оплата за кожний використаний гігабайт оперативної пам’яті на секунду; до того ж, є можливість впровадження через код будь-яким зручним способом. Логування теж не потребує зайвих зусиль. Справжній жах для DevOps. Це ж означає, що скоро їх робота — налаштування серверів, балансування навантаження, моніторинг, логування, візити до центрів обробки даних й інші приколи, пов’язані з розгортання веб-застосунків, — скоро буде нікому не потрібна.
Адаптування традиційних веб-застосунків для роботи на AWS Lambda це й досі не цілком тривіальне завдання, але варто в ньому розібратися і брати його до уваги наступного разу, коли десь треба буде впровадити веб-службу. І щоразу з ним буде простіше впоратися.
Отже, що ми тут розуміємо під «безсерверністю»? Ідеться про те, що більше нема потреби керувати серверами й довготривалими процесами. Ви просто завантажуєте код у хмару та виконуєте його коли й скільки разів потрібно, не переймаючись масштабуванням і оплачуючи лише процесорний час, об’єм пам’яті й інші ресурси, якими користуєтесь. Цей спосіб розгортання застосунків разюче відрізняється від звичного керування серверами й інфраструктурою, балансування навантаження й інших приколів, ніяк не пов’язаних із написанням коду та його розгортанням.
Python і Flask
Наразі є багато варіантів реалізації веб-фреймворків, і Python 3 у зв’язці з Flask — далеко не найгірший із них. Flask дає змогу створювати прості веб-служби з мінімумом шаблонного коду, а також згодиться як компонент для розробки складних веб-застосунків з одним застереженням: він краще пасує для API, ніж застосунків із рендерингом на стороні серверу, на відміну від Django, наприклад. Та все одно в наші часи здебільшого доводиться мати справу з односторінковими застосунками.
Python — мова, що зажила широкої підтримки та має багатий каталог бібліотек-модулів, яка дає змогу писати легкий для сприйняття і простий у супроводженні код. Якщо ж витратити трохи часу на налаштування контролю якості коду засобами mypy і flake8, можна забезпечити завчасну перевірку відповідності типів і відловлювання поширених помилок. І підтримка для цього всього є в AWS Lambda.
Як виглядає безсерверний застосунок на базі Flask
Щоб створити безсерверний застосунок на платформі Lambda, потрібен шаблон конфігурації CloudFormation, що б описував вашу архітектуру.
Платформа Lambda є лише одним зі складників цієї архітектури — це функція, що повертає певний результат, яку можна будь-коли викликати. Щоб скористатися нею як веб-службою, треба якось отримати до неї доступ з інтернету. AWS має службу API Gateway (APIGW), яка може очікувати, доки на кінцеву точку надійде HTTP(S)-запит, і зробити те, що потрібно. APIGW може мати записи для кожної кінцевої точки (POST /api/foo, GET /api/bar тощо) або проксіювати запити, що надходять на певний хост, до Lambda із додаванням певного префікса шляху, а потім інтерпретувати відповідь як HTTP-відповідь та передавати її ініціаторові запиту. У такому разі ця служба буде працювати як загальний інтерфейс шлюзу (common gateway interface, CGI) або інтерфейс шлюзу веб-сервера (web server gateway interface, WSGI), і якщо ваш веб-фреймворк може виконувати десеріалізацію проксійованих API Gateway запитів, а потім серіалізувати відповідь у формат, прийнятний для APIGW, для вашого застосунку вона буде іще одним «контейнером» веб-застосунку.
Для цього у Flask є спеціальне розширення — AWSGI. На прикладі нижче — все, що потрібно для того, щоб ваш застосунок зробити безсерверним:
Єдине, що тут є специфічного для Lambda — функція lambda_handler, яка передає Flask потрібний об’єкт WSGI-запиту, а потім перетворює відповідь у формат, потрібний AWSGI.
Отже, інфраструктура доволі очевидна — хостинг коду на Lambda, API Gateway для зворотного проксіювання, і таке інше. Звісно, ще потрібні деякі супутні дрібниці на зразок дозволів для керування ідентифікацією та доступом (identity and access management, IAM) та, мабуть, бази даних чи об’єктного сховища — Amazone S3 bucket, або чого там іще потребує ваш застосунок. Все це можна визначити в шаблоні CloudFormation.
У AWS CloudFormation наявні «трансформації» (transform), які спрощують налаштування таких речей, надаючи шаблонні ресурси для вашого шаблону. Шаблони в шаблонах — дійсно вдалий спосіб визначення конфігурації. Від вас потрібен лише необхідний мінімум дій.
Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello_world/build/ Handler: app.lambda_handler Runtime: python3.6 Events: HelloWorld: Type: Api Properties: Path: /hello Method: get
У підсумку ви отримуєте Lambda, на якій виконується код, завантажений у сховище S3 після виклику у /hello.
Розгортання
Розгортання відбувається не настільки гладко, як, наприклад, у випадку з Heroku, але ситуація поступово поліпшується. Хоча є кілька популярних інструментів для керування взаємодією систем у безсерверних конфігураціях на зразок Serverless Framework, але я поставив собі за мету зробити якнайбільше за допомогою власного інструментарію AWS.
AWS має службу, якою, мабуть, ніхто крім мене досі не скористався — CodeStar. Вона автоматично створює конвеєр (pipeline) для розгортання безсерверних застосунків, налаштовуючи CodePipeline, CodeBuild і CloudFormation так, щоб у підсумку отримати систему неперервної інтеграції та розгортання застосунків. Можна зробити так, щоб вона збирала застосунок щоразу, як ви щось додаєте у репозиторій на GitHub і автоматично повторно його розгортала за допомогою CloudFormation. Трансформація CodeStar CloudFormation додатково спрощує і так нескладний процес роботи з шаблонами.
Документація, присвячена CodeStar і супутній трансформації, доволі куца, тож розбиратися з ними — це для тих, хто не шукає легких шляхів. Втім, ця штука дійсно завантажує код у сховище S3 і налаштовує кілька ролей. Шаблон для CodeStar template.yml може виглядати якось так:
Flask: Type: AWS::Serverless::Function Properties: Timeout: 10 Handler: myapp/index.lambda_handler Runtime: python3.6 Role: Fn::ImportValue: !Join ['-', [!Ref 'ProjectId', !Ref 'AWS::Region', 'LambdaTrustRole']]
Якщо створити репозитарій із файлом myapp/index.py, що міститиме функцію lambda_handler на зразок наведеного вище обробника AWSGI, отримаєте безсерверний веб-застосунок, що буде автоматично оновлюватися щоразу, як ви завантажуєте код у свій репозитарій на GitHub і виконується CodeBuild.
І хоча завдяки всьому, що можуть запропонувати AWS і CloudFormation, CodeStar потенційно може спростити розробку безсерверних застосунків до рівня Heroku, але, здається, ним досі ніхто активно не намагався користуватися. Я спробував додати IAM-роль до своєї функції Lambda, але стикнувся з проблемою, що спантеличила навіть службу підтримки AWS. Кінець кінцем там підтвердили, що задокументованого способу налаштувати роль IAM немає.
«Після додаткового тестування і дослідження проблеми, я дійшов висновку, що наразі немає можливості налаштувати IAM-роль безсерверної функції Lambda засобами template.yml. Натомість доведеться надавати потрібні права IAM-ролі вручну. Втім, як ви й зауважили, роль функції Lambda створюється автоматично і тому немає можливості визначити саме ту IAM-роль, яка буде застосована».
Як тимчасове розв’язання проблеми, можна простежити як трансформація CodeStar (принцип роботи якої не дуже зрозумілий) складає назву ролі, та надати права саме потрібній ролі. Маю сумнів, що це підтримується чи десь задокументовано. Але служба підтримки обіцяла все виправити.
Розгортаючи Lambda, можна цілком обійтися й без CodeStar, але якщо бажаєте пошаманити над екзотичнішими службами AWS, вона може стати цінним інструментом, залежно від ваших потреб. Але це завжди задоволення — бачити як занедбані служби AWS з часом розвиваються та набирають популярності (маю на увазі CodeDeploy тощо); до того ж, уявіть, як захоплено будуть слухати друзі про місяці ламання списів зі службою підтримки AWS з приводу однієї-єдиної проблеми.
До речі, коли спробував за допомогою CodeBuild провести кілька тестів на своєму проекті, виявилося, що образу з python 3.6 тут нема, тож користі в цьому випадку з інструменту було нуль. Наразі вони, здається, його додали, тож тепер можна щось робити.
Інші варіанти розгортання
Якщо вам просто треба налаштувати Lambda на швидку руку, заморочуватися з системою неперервної інтеграції та розгортання і CloudFormation не доведеться. Налаштування для одиничного використання можна виконати вручну — треба лиш написати трохи коду, запустити його, й забути. Чудовий варіант для Slack-ботів і невеликих веб-служб, як на мене. Я зробив собі плагін для Sublime Text 3, щоб редагувати AWS Lambda. Він дає змогу редагувати Lambda прямо у Sublime і завантажує оновлену версію щоразу, як ви зберігаєте файл. Також, можна її викликати й дивитися, що вона повертає, не покидаючи Sublime, а також можна зручно додавати залежності до проекту за допомогою pip. Це значно спрощує роботу і точно зручніше, ніж користуватися веб-редактором функцій або розархівовувати й архівувати назад свій пакет щоразу, як треба змінити код.
Залежності
Свою Lambda можна завантажити як звичайний архів папки у форматі ZIP. У папці буде код вашого застосунку, а також всі дані й залежності, які йому потрібні (десь там будуть також ваші мрії й сподівання). Якщо застосунок потребує додаткових бібліотек (на додачу до boto3, яка в Lambda вже є), їх потрібно буде додати.
Щоб спростити розгортання, можна скопіювати бібліотеки, інстальовані у віртуальне оточення проекту. Якщо бібліотеки містять апаратно-залежний код, його треба компілювати на linux-машині з архітектурою amd64, бо на такій працює Lambda. Деякі інструменти автоматизують цей момент за допомогою Docker.
Якщо потрібен зручніший варіант, можна створити папку, в яку можна швидко закинути все, що потрібно.
Для свого проекту я написав невеличкий скрипт, за допомогою якого можна встановлювати пакети через pip у визначену папку («vendor/»). Нічого надзвичайного, насправді. Він просто виконує команду «pip install -t ...» і видаляє потім непотрібні файли.
На початку файлу __init__.py мого застосунку я додаю постачальника та вказую кореневий шлях до PYTHONPATH. У підсумку я отримую невеличке віртуальне оточення, в якому можу користуватися всіма модулями, інстальованими у директорії постачальника.
import os import sys vendor_path = os.path.abspath(os.path.join(__file__, '..', '..', 'vendor')) lib_path = os.path.abspath(os.path.join(__file__, '..', '..')) sys.path.append(lib_path) sys.path.append(vendor_path) from flask import Flask ...
А потім можна просто імпортувати всі потрібні залежності, які будуть додані у пакет.
Збираємо докупи
Я створив простий веб-застосунок, щоб поекспериментувати з CodeStar. Він дає користувачам змогу ставити питання й відповідати на них. Спочатку це був застосунок для Slack, але з ним все сталося не так, як гадалося.
Ліричний відступ: Я мав на меті дати змогу будь-якому користувачу Slack ставити питання й відповідати на них, але оскільки застосунок був встановлений для Slack-команди, що складалася переважно з тролів та іншого непотребу, рецензент від Slack був неприємно вражений якістю та глибиною відповідей, які він отримував під час тестування застосунку. І це цілком і повністю моя провина. Він і досі доступний у каталозі застосунків для Slack, але оскільки йому не надано права для кроскомандного використання (щось там писали про «неприйнятність використання на робочому місці»), користі зі взаємодії зі Slack небагато.
Цей застосунок не робить нічого надзвичайного: просто дає змогу користувачам ставити питання і відповідати на них. Уперше я його реалізував як веб-службу, сумісну з Slack webhooks та Slash Commands HTTP API, згодом додавши підтримку REST API для вебу.
Був би це серйозний проект, я б зробив базу даних на PostgreSQL, але ж я хотів не просто навчитися добре проектувати безсерверні застосунки на базі Flask — треба було й мінімізувати витрати на хостинг. На жаль, PostgreSQL не можна наразі назвати цілком безсерверним рішенням, тож на AWS доведеться витрачати щонайменше кілька десятків доларів на місяць, якщо бажаєте, щоб ваш PostgreSQL-сервер працював не на безплатному мікроінстансі EC2. Отже, я вирішив скористатися NoSQL-рішенням від AWS — DynamoDB. Це доволі незручне сховище колекцій «ключ-значення» з документацією для boto3 написаною якимось садистом, але недороге та добре масштабується без зайвих зусиль. Принаймні, у теорії. Але на практиці це повна фігня.
За кілька доларів на місяць з DynamoDB ви отримуєте таблиці та індекси, і, на мою думку, можна цілком обійтися однією-двома, якщо не заморочуватися чимось на зразок реляційної бази даних. Додайте ще безкоштовні мільйон запитів і 400,000 Гбайт-секунд обчислювального часу на місяць на Lambda, і ви отримаєте місце, де можна виконувати код і зберігати дані майже задарма. І горизонтальне масштабування тут не має потребувати додаткових уваги та зусиль. Звісно, насправді все не настільки просто, але чудово знати, що таке можливо. Щонайменше мені більше не доведеться налаштовувати веб-сервер або адмініструвати машину просто для того, щоб розгорнути веб-застосунок, і доплачуватиму за це я (дуже) невелику суму. Одна з найбільших переваг безсерверних застосунків — можливість налаштувати все один раз і більше цим не перейматися. Якщо перший раз запрацювало — працюватиме й надалі. Вам не доведеться перейматися відмовою дисків і резервним копіюванням і думати, як впоратися з піковими навантаженнями та простоями. Звісно, AWS — не втілення довершеності, але коли йдеться про забезпечення цілодобової роботи моїх «лямбд», я їм довіряю більше, ніж багатьом людям і навіть собі. Особливо собі.
Таємні ключі
Як і з будь-яким розгортанням застосунку, вам знадобиться сховище для таємних ключів. Lambda не потребує ключа API AWS, бо вона виконується з IAM-роллю, якій можна надати доступ до всіх потрібних служб. Для зовнішніх служб можна використовувати сховище AWS SSM Parameter Store. Воно дає змогу зберігати таємні ключі й отримувати до них доступ ролям або користувачам, які мають дозвіл на їх зчитування. Це чудовий варіант для зберігання ключів API, токенів тощо.
Оскільки ми користуємося Flask, SSM Parameter Store можна легко інтегрувати у файлі config.py фреймворку:
import boto3 ssm = boto3.client('ssm') def get_ssm_param(param_name: str, required: bool = True) -> str: """Get an encrypted AWS Systems Manger secret.""" response = ssm.get_parameters( Names=[param_name], WithDecryption=True, ) if not response['Parameters'] or not response['Parameters'][0] or not response['Parameters'][0]['Value']: if not required: return None raise Exception( f"Configuration error: missing AWS SSM parameter: {param_name}") return response['Parameters'][0]['Value'] TWILIO_API_SID = get_ssm_param('qanda_twilio_account_sid') TWILIO_API_SECRET = get_ssm_param('qanda_twilio_account_secret') SLACK_OAUTH_CLIENT_ID = get_ssm_param('qa_slack_oauth_client_id') SLACK_OAUTH_CLIENT_SECRET = get_ssm_param('qa_slack_oauth_client_secret') SLACK_VERIFICATION_TOKEN = get_ssm_param('qanda_slack_verification_token') SLACK_LOG_ENDPOINT = get_ssm_param('qanda_slack_log_webhook', required=False)
Таємні ключі: у безпеці.
Локальне виконання
Оскільки «лямбди» виконуються в AWS, можна припустити, що розгортання й тестування кожної заміни в коді за допомогою AWS — обтяжливий процес. Дійсно, було б дуже прикро, якщо б це дійсно доводилося робити. Є такий проект для AWS — SAM-CLI (інтерфейс командного рядка для застосунків безсерверної моделі). За допомогою докерних образів Lambda можна викликати застосунок у такому ж оточенні, в якому б він працював на Lambda. На нього можна передати файл JSON, де описаний запит для Lambda і подивитися відповідь, або запустити його як сервер, до якого можна під’єднатися як до будь-якого локального веб-сервера розробки. Вам також знадобиться ключ API AWS, якщо бажаєте, щоб застосунок використовував служби AWS, оскільки він виконується на локальній машині без підтримки ролі в інстансі AWS.
Ще приклади
Узагальнюючи написане вище, ось кілька думок, які треба брати до уваги під час створення та розгортання безсерверних веб-застосунків. Мені сподобалося, як усе зійшлося в моєму навчальному проекті QandA і пропоную вам поглянути на його структуру й вихідний код , якщо вам потрібен повноцінний приклад. Я міг би також заглибитися в подробиці щодо того, як я структурував Flask-застосунок, але це мало стосується Lambda чи безсерверного аспекту — якщо вам це цікаво, просто подивіться код.
Lambda ще на початковому етапі розвитку, і з нею працювати не так легко, як із Heroku. Але вона має й незаперечну перевагу: можливість просто «виконувати код у хмарі», не переймаючись керуванням сервером, майже задарма. Витративши трохи часу на налаштування безсерверного застосунку, ви матимете простий спосіб виконувати код в інтернеті, зосередившись лише на рівні «запит -> код застосунку -> відповідь». На мою думку, краще вже писати код і файли налаштувань, а не заморочуватися керуванням інфраструктурою й серверами.