В рамках практической работы вы пройдете полноценный путь разработки serverless-приложения с использованием современных инструментов и сервисов. Вам предстоит создать небольшой, но функциональный сервис фильмов на языке typescript и платформе Node.js и развернуть его в Яндекс.Облаке.
На протяжении всей работы вы не столкнетесь ни разу с такими терминами как виртуальная машина или сервер!
Перед тем как начать практическую работу вам необходимо установить и настроить следующие инструменты:
- WebStorm(или любая другая среда разработки с поддержкой typescript)
- Git
- Typescript
- Node.js
- Docker
- Yandex Cloud CLI(yc)
- Terraform
- Amazon Web Services(AWS) CLI
Подробное описание того, как это сделать, приведено здесь.
- Начало работы
- Создание базы данных
- Реализация CRUD-операций
- Разработка REST API
- Импорт фильмов и картинок
- Аутентификация и авторизация
- Проставление оценок и подсчет рейтинга
- Веб-интерфейс
- Скачайте проект из git-репозитория и откройте его в WebStorm:
git clone <ссылка на репозиторий>
- Выполните команду:
yc config profile get <имя профиля>
- Скопируйте из вывода идентификаторы облака, каталога и oauth-токен и вставьте их в файл provider.tf в соответствующие поля. А также экспортируйте идентификатор каталога в переменную окружения:
export FOLDER_ID=<идентификатор каталога>
echo $FOLDER_ID
- Выполните в терминале команду инициализации terraform из-под директории
deploy
:
cd deploy
terraform init
В дальнейшем все команды terraform
необходимо выполнять из-под каталога deploy
.
Веб-приложения можно проектировать и разрабатывать сверху вниз или снизу вверх. Это спорная тема, но мы остановимся на
втором подходе. А это означает, что вначале нам надо придумать схему или модель хранения наших данных, после этого
определиться с СУБД, а затем создать базу данных приложения и соответствующие нашей схеме таблицы. В арсенале
serverless-стека Яндекс.Облака имеется Yandex Database со специальным serverless-режимом работы. Ее мы и будем
использовать здесь. Для веб-приложения потребуются две таблицы: movies
для хранения фильмов и votes
для хранения оценок пользователей. Каждая запись в таблице movies
будет содержать идентификатор фильма и конечный
набор аттрибутов, аналогично с оценками. Раз мы определились со схемой и СУБД, давайте создадим базу данных:
- В файле ydb.tf описана конфигурация необходимой для хранения фильмов базы данных Yandex Database. Для ее развертывания в Яндекс.Облаке выполните команду:
terraform apply -target=yandex_ydb_database_serverless.movies_database
После завершения команды в вашем каталоге будет создана база данных movies-database
. В консоли в
переменной movies_database_document_api_endpoint
будет напечатан ее эндпоинт, а в переменной movies_database_path
-
относительный путь. Экспортируйте эти значения в переменные окружения:
export DOCUMENT_API_ENDPOINT=<movies_database_document_api_endpoint>
echo $DOCUMENT_API_ENDPOINT
export MOVIES_DATABASE_PATH=<movies_database_path>
echo $MOVIES_DATABASE_PATH
- Чтобы создать таблицы, выполните команды, приведенные ниже:
aws dynamodb create-table \
--table-name movies \
--attribute-definitions \
AttributeName=id,AttributeType=N \
AttributeName=title,AttributeType=S \
AttributeName=type,AttributeType=S \
AttributeName=original_title,AttributeType=S \
AttributeName=original_language,AttributeType=S \
AttributeName=release_date,AttributeType=S \
AttributeName=poster_path,AttributeType=S \
AttributeName=popularity,AttributeType=N \
AttributeName=video,AttributeType=S \
AttributeName=vote_count,AttributeType=N \
AttributeName=vote_average,AttributeType=N \
AttributeName=genres,AttributeType=S \
AttributeName=backdrop_path,AttributeType=S \
AttributeName=adult,AttributeType=S \
AttributeName=overview,AttributeType=S \
--key-schema \
AttributeName=id,KeyType=HASH \
--global-secondary-indexes \
"[
{
\"IndexName\": \"PopularityIndex\",
\"KeySchema\": [{\"AttributeName\":\"type\",\"KeyType\":\"HASH\"}, {\"AttributeName\":\"popularity\",\"KeyType\":\"RANGE\"}],
\"Projection\":{
\"ProjectionType\":\"ALL\"
}
}
]" \
--endpoint ${DOCUMENT_API_ENDPOINT}
aws dynamodb create-table \
--table-name votes \
--attribute-definitions \
AttributeName=id,AttributeType=S \
AttributeName=user_id,AttributeType=S \
AttributeName=movie_id,AttributeType=N \
AttributeName=value,AttributeType=N \
--key-schema \
AttributeName=id,KeyType=HASH \
--global-secondary-indexes \
"[
{
\"IndexName\": \"MovieIndex\",
\"KeySchema\": [{\"AttributeName\":\"movie_id\",\"KeyType\":\"HASH\"}],
\"Projection\":{
\"ProjectionType\":\"ALL\"
}
}
]" \
--endpoint ${DOCUMENT_API_ENDPOINT}
Чтобы проверить, что таблицы создались, можно выполнить команду describe-table
как показано ниже:
aws dynamodb describe-table --table-name movies --endpoint ${DOCUMENT_API_ENDPOINT}
aws dynamodb describe-table --table-name votes --endpoint ${DOCUMENT_API_ENDPOINT}
Обратите внимание, что для таблицы movies
необходимо поддерживать два индекса. Первый индекс для быстрого поиска
фильма по его идентификатору, а второй - для сортировки фильмов по популярности. Аналогично для таблицы votes
у нас
будет два индекса: один для поиска оценки конкретного пользователя по конкретному фильму, другой для получения всех
оценок фильма.
Практически каждое приложение или сервис имеет свой слой работы с данными или базой данных. Этот слой мы будем
использовать при реализации сервиса каждый раз, когда нам необходимо будет создавать, получать, обновлять или удалять
фильмы и оценки. Эти операции еще обычно называют CRUD-операциями. В качестве API для работы с базой данных будем для
простоты использовать AWS DynamoDB Document API, а именно
библиотеку AWS SDK for JavaScript v3, его
реализующую. В файле model.ts через интерфейс typescript определены модели фильма Movie
и
оценки Vote
. В файле repository.ts реализованы основные CRUD-операции для работы с этими
сущностями. Обратите внимание, что для авторизации при выполнении этих операций используются IAM-токены, выписываемые
для сервисного аккаунта, который мы создадим позже. Для получения IAM-токена перед выполнением операции
вызывается сервис метаданных. В этом разделе вы
установите необходимы зависимости и скомпилируете javascript код.
- Установите необходимые зависимости, выполнив команду из корня проекта:
npm ci
После выполнения команды в проекте появится директория node_modules
, в которой будут находиться все необходимые
зависимости.
- Запустите сборку проекта:
npm run build
После выполнения команды в проекте появится директория dist
, в которой будут находиться скомпилированные js-файлы.
Ключевым компонентом практически любого современного веб-сервиса является его REST API(учитываем, что пока еще подавляющее большинство приложений используют именно REST-подход при реализации своего API). При его разработке важно понимать, какие у вашего приложения будут сценарии использования и на каких клиентских платформах( веб/мобильные/тв и т.д.) они будут реализовываться. В нашем случае мы сделаем простой веб-интерфейс, в котором будет страница со списком фильмов и страница с детальной информацией про фильм. В файле openapi/api.yaml уже подготовлена OpenAPI-спецификация REST-сервиса. Обратите внимание на то, что там описаны основные операции работы с фильмами и оценками. Для реализации сервиса в соответствии с этой спецификацией будем использовать библиотеку openapi-backend в связке с фреймворком express. В файле app.ts инициализируются необходимые классы, прописывается маппинг операций и запускается http-сервис. Приложение будет собираться в виде docker-образа и разворачиваться в Serverless Containers. Последовательно пройдите все шаги, необходимые для развертывания REST API:
- Для начала создайте сервисный аккаунт
movies-api-sa
, от которого будет работать приложение. Его описание приведено в файле sa.tf. Чтобы сделать это, выполните в консоли команду:
terraform apply -target=yandex_iam_service_account.movies_api_sa
В выводе команды в переменной movies_api_sa_id
вы увидите идентификатор созданного сервисного аккаунта. Добавим его в
переменную окружения:
export MOVIES_API_SA_ID=<movies_api_sa_id>
echo $MOVIES_API_SA_ID
Сервисный аккаунт будет использоваться приложением для следующих целей:
- вызова контейнера в Serverless Containers,
- выполнения операций к базе данных в Yandex Database
- вызова функции в Cloud Functions
- работы c Object Storage
- записи в поток Yandex DataStreams
Чтобы выполнять эти действия, выдадим этому сервисному аккаунту необходимые роли (для простоты выдадим роли на весь каталог, а не на каждый ресурс в отдельности). Для этого последовательно выполните следующие команды:
yc resource-manager folder add-access-binding ${FOLDER_ID} --role ydb.admin --subject serviceAccount:${MOVIES_API_SA_ID}
yc resource-manager folder add-access-binding ${FOLDER_ID} --role container-registry.images.puller --subject serviceAccount:${MOVIES_API_SA_ID}
yc resource-manager folder add-access-binding ${FOLDER_ID} --role serverless.containers.invoker --subject serviceAccount:${MOVIES_API_SA_ID}
yc resource-manager folder add-access-binding ${FOLDER_ID} --role serverless.functions.invoker --subject serviceAccount:${MOVIES_API_SA_ID}
yc resource-manager folder add-access-binding ${FOLDER_ID} --role storage.editor --subject serviceAccount:${MOVIES_API_SA_ID}
yc resource-manager folder add-access-binding ${FOLDER_ID} --role yds.admin --subject serviceAccount:${MOVIES_API_SA_ID}
Пропишите идентификатор сервисного аккаунта, созданного выше, в начале спецификации openapi/api.yaml
в поле x-yc-apigateway.service_account_id
.
- Теперь создайте реестр и репозиторий docker-образов в Container Registry, куда будет выкладываться образ приложения. Необходимые ресурсы описаны в файле container-registry.tf. Для этого выполните команды:
terraform apply -target=yandex_container_registry.default
terraform apply -target=yandex_container_repository.movies_api_repository
В выводе команды в переменной movies_api_repository_name
вы увидите название репозитория, которое ниже будет
использоваться при загрузке образа. Экспортируйте его в переменную окружения:
export MOVIES_API_REPOSITORY_NAME=<movies_api_repository_name>
echo $MOVIES_API_REPOSITORY_NAME
Далее сконфигурируйте docker для работы с только что созданным репозиторием с помощью команды:
yc container registry configure-docker
- Соберите docker-образ приложения(файл конфигурации docker-образа традиционно называется Dockerfile) и загрузите его в созданный на предыдущем шаге репозиторий. Для этого выполните последовательно следующие команды из корня проекта:
docker build -t ${MOVIES_API_REPOSITORY_NAME}:0.0.1 .
docker push ${MOVIES_API_REPOSITORY_NAME}:0.0.1
- Создайте serverless-контейнер, который будет запускаться из выше созданного образа:
yc sls container create --name movies-api-container --folder-id ${FOLDER_ID}
В выводе команды вы увидите идентификатор созданного контейнера, экспортируйте его в переменную окружения:
export MOVIES_API_CONTAINER_ID=<идентификатор контейнера>
echo $MOVIES_API_CONTAINER_ID
Разверните ревизию контейнера с версией образа 0.0.1
:
yc sls container revisions deploy \
--folder-id ${FOLDER_ID} \
--container-id ${MOVIES_API_CONTAINER_ID} \
--memory 512M \
--cores 1 \
--execution-timeout 5s \
--concurrency 4 \
--environment AWS_ACCESS_KEY_ID=FAKE_AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY=FAKE_AWS_SECRET_ACCESS_KEY,DOCUMENT_API_ENDPOINT=${DOCUMENT_API_ENDPOINT} \
--service-account-id ${MOVIES_API_SA_ID} \
--image ${MOVIES_API_REPOSITORY_NAME}:0.0.1
Обратите внимание на то, что мы сконфигурировали параметр concurrency, для того что бы один запущенный экземпляр
serverless-контейнера мог конкурентно обрабатывать несколько запросов. Также обратите внимание, что мы через переменную
окружения DOCUMENT_API_ENDPOINT
передаем внутрь нашего приложения эндпоинт базы данных.
- Чтобы сервис полноценно заработал и был доступен из интернета, необходимо
развернуть API Gateway. Замените
${MOVIES_API_CONTAINER_ID}
на идентификатор созданного контейнера во всех интеграциях в спецификации openapi.yaml. В файле api-gateway.tf сконфигурирован API-шлюз, реализующий спецификацию. Для того чтобы создать его в облаке выполните команду:
terraform apply -target=yandex_api_gateway.movies_api_gateway
В выводе команды в переменной movies_api_gateway_domain
вы увидите доменное имя, на котором развернут созданный
API-шлюз. Экспортируйте его в переменную окружения:
export MOVIES_API_GATEWAY_DOMAIN=<movies_api_gateway_domain>
echo $MOVIES_API_GATEWAY_DOMAIN
- Проверим работу развернутого сервиса. Для этого сделаем http-запрос с помощью команды
curl
:
curl "${MOVIES_API_GATEWAY_DOMAIN}/movies?limit=10"
В ответе должен прийти пустой список []
, так как пока никаких данных в базе нет. Также вы можете загрузить
спецификацию в Postman или SwaggerHub, добавив в нее
в секцию servers
адрес созданного API-шлюза из переменной ${MOVIES_API_GATEWAY_DOMAIN}
. Это позволит вам удобно
делать вызовы к REST API.
Зайдите в веб-консоль на страницу serverless-контейнера и в разделе Логи
вы увидите сообщение, соответствующее вызову.
Также вы можете зайти в раздел Мониторинг
и увидеть вызов на графиках. Аналогично на странице API-шлюза вы можете
посмотреть логи и графики вызовов в соответствующих разделах.
Наполнением любого сайта или веб-приложения является его контент. Контентом нашего приложения являются данные о фильмах.
В этом разделе мы научим наше приложение импортировать данные из других источников. Как вы знаете даже
Кинопоиск изначально был всего лишь парсером IMDB. Мы же наполним
наше приложение данными из открытой базы фильмов TMDB. Для этого разработаем функцию
импорта данных и настроим триггер по расписанию, который будет запускать эту функцию. Чтобы не заставлять вас
регистрироваться в TMDB и получать api-ключ, мы уже это сделали заранее и выгрузили в Object Storage файл в формате
json tmdb.json с необходимыми данными. Будем считать, что этот
файл обновляется с какой-то периодичностью. Нашему приложению всего лишь необходимо регулярно забирать его, читать и
обновлять информацию о фильмах в базе данных. В файле import.ts уже написан код функции, выполняющий
эту задачу. Вместе с загрузкой данных в movies-database
эта функция скачивает файлы постеров и заставок фильмов и
кладет их в отдельный бакет в Object Storage. Веб-интерфейс приложения будет для каждого фильма показывать его постер и
заставку, загружая их из этого бакета.
- Cоздайте бакет в Object Storage. Необходимая конфигурация описана в файле object-storage.tf. Для применения конфигурации выполните команду:
terraform apply -target=yandex_storage_bucket.movies_images_bucket
В выводе команды в переменной movies_images_bucket
будет напечатано имя бакета. Сохраните его в переменной окружения:
export MOVIES_IMAGES_BUCKET=<movies_images_bucket>
echo $MOVIES_IMAGES_BUCKET
- Теперь разверните функцию импорта, необходимая конфигурация приведена в
файле import-function.tf. Функция будет запускаться на
nodejs-рантайме
nodejs16-preview
, в котором существенно уменьшено время холодного старта. Обратите внимание на переменные окружения в параметреenvironment
, среди них естьIMAGES_BUCKET_NAME
- имя созданного выше бакета, в который функция будет загружать изображения. Соберем и запакуем код функции в zip-архив, а потом развернем ее, выполнив следующие команды из-под корня проекта:
npm run package-import && cd deploy
terraform apply -target=yandex_function.import_function
- Теперь настройте триггер, который будет запускать функцию импорта по расписанию. Триггер как и функция уже
сконфигурирован в файле import-function.tf. Обратите внимание на крон-выражение в
параметре
cron_expression
, согласно которому функция будет запускаться раз в час. Для создания и запуска триггера достаточно выполнить команду:
terraform apply -target=yandex_function_trigger.import_trigger
- Запустите функцию и подождите пока она отработает, а затем снова сделайте запрос к REST API:
yc sls function invoke --name import-function
# {"statusCode":200,"body":"Imported 100 movies from TMDB","isBase64Encoded":false}
curl "${MOVIES_API_GATEWAY_DOMAIN}/movies?limit=10"
В ответе вы увидите массив json-объектов с фильмами, которые были загружены в базу данных.