Докеризованные лямбды

Цель этого поста - настроить бессерверную инфраструктуру, управляемую в коде, для обслуживания прогнозов контейнерной модели машинного обучения через Rest API так просто, как:

$ curl \
$  -X POST \
$  --header "Content-Type: application/json" \
$  --data '{"sepal_length": 5.9, "sepal_width": 3, "petal_length": 5.1, "petal_width": 1.8}' \
$  https://my-api.execute-api.eu-central-1.amazonaws.com/predict/
{"prediction": {"label": "virginica", "probability": 0.9997}}

Мы будем использовать Terraform для управления нашей инфраструктурой, включая AWS ECR, S3, Lambda и API Gateway. Мы будем использовать AWS Lambda для запуска кода модели фактически в контейнере, что является очень недавней функцией. Мы будем использовать AWS API Gateway для обслуживания модели через Rest API. Сам артефакт модели будет жить в S3. Вы можете найти полный код здесь.

Предпосылки

Мы используем Terraform v0.14.0 и aws-cli/1.18.206 Python/3.7.9 Darwin/19.6.0 botocore/1.19.46.

Нам необходимо пройти аутентификацию в AWS, чтобы:

  • настроить инфраструктуру с помощью Terraform.
  • обучить модель и сохранить результирующий артефакт модели в S3
  • протестировать инфраструктуру с помощью AWS CLI (здесь:)

Учетные данные AWS можно настроить в файле учетных данных, то есть ~/.aws/credentials, используя профиль с именем lambda-model:

[lambda-model]
aws_access_key_id=...
aws_secret_access_key=...
region=eu-central-1

Это позволит нам указать Terraform, а также интерфейс командной строки AWS, какие учетные данные использовать. Сама функция Lambda будет аутентифицироваться с использованием роли, поэтому явные учетные данные не требуются.

Кроме того, нам нужно определить регион, имя корзины и некоторые другие переменные, они также определены в переменных Terraform, как мы увидим позже:

export AWS_REGION=$(aws --profile lambda-model configure get region)
export BUCKET_NAME="my-lambda-model-bucket"
export LAMBDA_FUNCTION_NAME="my-lambda-model-function"
export API_NAME="my-lambda-model-api"
export IMAGE_NAME="my-lambda-model"
export IMAGE_TAG="latest"

Создание контейнерной модели

Давайте построим очень простую контейнерную модель на iris наборе данных. Мы определим:

  • model.py: фактический код модели
  • utils.py: служебные функции
  • train.py: сценарий для запуска обучения модели
  • test.py: сценарий для генерации прогнозов (в целях тестирования)
  • app.py: обработчик Lambda

Чтобы сохранить артефакт модели и загрузить данные для обучения модели, мы определим несколько вспомогательных функций для связи с S3 и загрузки файлов с обучающими данными из общедоступных конечных точек в utils.py:

Кроме того, нам нужен класс-оболочка для нашей модели:

  • обучить его на внешних данных
  • сохранить состояние и сохранить и загрузить артефакт модели
  • передавать полезные данные для вывода

Это будет определено в model.py:

Для обучения и прогнозирования без реальной инфраструктуры лямбда мы также настроим два скрипта, train.py и predict.py. Сценарий обучения может быть очень простым, мы могли бы также передать методу train другие источники данных.

from model import ModelWrapper

model_wrapper = ModelWrapper() 
model_wrapper.train()

И простой predict.py, который выводит прогнозы на консоль:

import json
import sys
from model import ModelWrapper

model_wrapper = ModelWrapper()
model_wrapper.load_model()
data = json.loads(sys.argv[1])
print(f"Data: {data}")
prediction = model_wrapper.predict(data=data)
print(f"Prediction: {prediction}")

Наконец, нам нужен обработчик для передачи данных в оболочку модели. Это то, что будет вызываться лямбда-функцией. Мы будем максимально минималистичны, обработчик просто передаст запрос в оболочку и преобразует возвращаемые прогнозы в выходной формат, ожидаемый API Gateway:

Мы поместим все это в Docker (точнее Dockerfile) и воспользуемся одним из базовых образов AWS Lambda:

FROM public.ecr.aws/lambda/python:3.8
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app.py utils.py model.py train.py predict.py ./
CMD ["app.handler"]

Создание ресурсов ECR и S3

Давайте теперь определим репозиторий ECR и корзину S3 через Terraform. Правильно организованный код Terraform можно найти в репозитории GitHub.

Мы определяем некоторые конфигурации (variables и locals) и AWS как provider. В качестве альтернативы переменные также могут быть загружены из среды.

Кроме того, репозиторий S3 и ECR:

Давайте создадим нашу корзину S3 и репозиторий ECR:

(cd terraform &&  \
  terraform apply \
  -target=aws_ecr_repository.lambda_model_repository \
  -target=aws_s3_bucket.lambda_model_bucket)

Сборка и отправка образа докера

Теперь мы можем создать наш образ докера и отправить его в репозиторий (в качестве альтернативы это можно сделать в null_resource provisioner в Terraform). Мы экспортируем идентификатор реестра, чтобы создать URI изображения, куда мы хотим отправить изображение:

export REGISTRY_ID=$(aws ecr \
  --profile lambda-model \
  describe-repositories \
  --query 'repositories[?repositoryName == `'$IMAGE_NAME'`].registryId' \
  --output text)
export IMAGE_URI=${REGISTRY_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${IMAGE_NAME}
# ecr login
$(aws --profile lambda-model \
  ecr get-login \
  --region $AWS_REGION \
  --registry-ids $REGISTRY_ID \
  --no-include-email)

Теперь строить и продвигать так же просто, как:

(cd app && \
  docker build -t $IMAGE_URI . && \
  docker push $IMAGE_URI:$IMAGE_TAG)

Обучение модели

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

docker run \
  -v ~/.aws:/root/.aws \
  -e AWS_PROFILE=lambda-model \
  -e BUCKET_NAME=$BUCKET_NAME \
  --entrypoint=python \
  $IMAGE_URI:$IMAGE_TAG \
  train.py
# Loading data.
# Creating model.
# Fitting model with 150 datapoints.
# Saving model.

Тестирование модели

Используя точку входа predict.py, мы также можем протестировать его с некоторыми данными:

docker run \
  -v ~/.aws:/root/.aws \
  -e AWS_PROFILE=lambda-model \
  -e BUCKET_NAME=$BUCKET_NAME \
  --entrypoint=python \
  $IMAGE_URI:$IMAGE_TAG \
  predict.py \
  '{"sepal_length": 5.1, "sepal_width": 3.5, "petal_length": 1.4, "petal_width": 0.2}'
# Loading model.
# Data: {'sepal_length': 5.1, 'sepal_width': 3.5, 'petal_length': 1.4, 'petal_width': 0.2}
# Prediction: ('setosa', 0.9999555689374946)

Планирование нашей основной инфраструктуры с Terraform

Теперь мы можем спланировать логическую часть инфраструктуры, настройку Lambda & API Gateway:

  • функция Lambda, включая роль и политику для доступа к S3 и создания журналов,
  • API Gateway, включая необходимые разрешения и настройки.

Теперь мы можем применить это снова, используя Terraform CLI, что займет около минуты.

(cd terraform && terraform apply)

Тестирование инфраструктуры

Чтобы проверить функцию Lambda, мы можем вызвать ее с помощью интерфейса командной строки AWS и сохранить ответ на response.json:

aws --profile lambda-model \
  lambda \
  invoke \
  --function-name $LAMBDA_FUNCTION_NAME \
  --payload '{"body": {"sepal_length": 5.9, "sepal_width": 3, "petal_length": 5.1, "petal_width": 1.8}}' \
  response.json
# {
#     "StatusCode": 200,
#     "ExecutedVersion": "$LATEST"
# }

response.json будет выглядеть так:

{
    "statusCode": 200,
    "body": "{\"prediction\": {\"label\": \"virginica\", \"probability\": 0.9997}}",
    "isBase64Encoded": false
}

И мы также можем протестировать наш API, используя curl или python. Сначала нам нужно узнать URL-адрес нашей конечной точки, например, снова с помощью интерфейса командной строки AWS или, в качестве альтернативы, распечатки вывода Terraform.

export ENDPOINT_ID=$(aws \
  --profile lambda-model \
  apigateway \
  get-rest-apis \
  --query 'items[?name == `'$API_NAME'`].id' \
  --output text)
export ENDPOINT_URL=https://${ENDPOINT_ID}.execute-api.${AWS_REGION}.amazonaws.com/predict
curl \
  -X POST \
  --header "Content-Type: application/json" \
  --data '{"sepal_length": 5.9, "sepal_width": 3, "petal_length": 5.1, "petal_width": 1.8}' \
  $ENDPOINT_URL
# {"prediction": {"label": "virginica", "probability": 0.9997}}

В качестве альтернативы мы можем отправлять запросы POST с помощью python:

import requests
import os

endpoint_url = os.environ['ENDPOINT_URL']
data = {"sepal_length": 5.9, "sepal_width": 3, "petal_length": 5.1, "petal_width": 1.8}
req = requests.post(endpoint_url, json=data)
req.json()

Больше замечаний

Чтобы обновить образ контейнера, мы можем снова использовать CLI:

aws --profile lambda-model \
  lambda \
  update-function-code \
  --function-name $LAMBDA_FUNCTION_NAME \
  --image-uri $IMAGE_URI:$IMAGE_TAG

Если мы хотим удалить нашу инфраструктуру, мы должны сначала очистить нашу корзину, после чего мы можем уничтожить наши ресурсы:

aws s3 --profile lambda-model rm s3://${BUCKET_NAME}/model.pkl
(cd terraform && terraform destroy)

Заключение

Благодаря новой функции контейнерных Lambdas стало еще проще развертывать модели машинного обучения в бессерверной среде AWS. Есть много альтернатив AWS этому (ECS, Fargate, Sagemaker), но Lambda поставляется с множеством готовых инструментов, например, с ведением журнала и мониторингом на основе запросов, и с легкостью позволяет быстро создавать прототипы. Тем не менее, у него также есть некоторые недостатки, например, накладные расходы, связанные с задержкой запроса и использование некоторой проприетарной облачной службы, которую нельзя полностью настроить.

Еще одно преимущество состоит в том, что контейнеризация позволяет нам изолировать код машинного обучения и правильно поддерживать зависимости пакетов. Если мы сведем код обработчика к минимуму, мы сможем тщательно протестировать образ и убедиться, что наша среда разработки очень близка к производственной инфраструктуре. В то же время мы не привязаны к технологии AWS - мы можем очень легко заменить обработчик на наш собственный веб-фреймворк и развернуть его в Kubernetes.

Наконец, мы потенциально можем улучшить инфраструктуру нашей модели, запустив обучение удаленно (например, с помощью ECS), добавив управление версиями и предупреждения CloudWatch. Если нужно, мы могли бы добавить процесс, чтобы Лямбда оставалась теплой, поскольку холодный старт занимает несколько секунд. Мы также должны добавить аутентификацию в конечную точку.

Первоначально опубликовано на https://blog.telsemeyer.com.