Шесть способов укрепить ваш API обслуживания моделей с помощью тестов и сканирования

Собираетесь в производство? Усильте свой API с помощью этих бесплатных ресурсов!

Привет друзья! Я вернулся! Приносим извинения за двухмесячный перерыв. Летом у меня всегда пропадает желание публиковать что-то новое, но теперь, когда у меня есть новый Mac Mini, который поддерживает мою работу, я возвращаюсь с новым контентом.

Как инженер по машинному обучению в компании из списка Fortune 50, я в первую очередь отвечаю за развертывание моделей машинного обучения в производственной среде для использования в нескольких системах уровня предприятия. Большинство этих развертываний имеют форму API обслуживания моделей. API принимает набор данных, генерирует вывод на основе этих данных и возвращает результаты вывода обратно вызывающей стороне.

Учитывая, что эти API-интерфейсы, обслуживающие модели, доступны большому количеству людей, очень важно убедиться, что эти API-интерфейсы укреплены таким образом, чтобы мы могли попытаться гарантировать 100% время безотказной работы. С этой целью мы стремимся укрепить наши API-интерфейсы, выполняя ряд различных тестов и сканирований безопасности, которые показывают, что мы тщательно учли все факторы риска перед развертыванием. Кроме того, эти результаты тестирования / сканирования сохраняются в общем репозитории на случай, если какие-либо аудиторы когда-либо будут заинтересованы в том, чтобы ощутимо увидеть «свидетельство тестирования» для этих API.

Конечно, существует множество различных инструментов и решений от поставщиков для поддержки этой работы, но в этой статье я сосредоточусь на бесплатных ресурсах. Если вы крупная корпоративная компания, я мог бы посоветовать вам изучить решения некоторых поставщиков, но этот пост, по крайней мере, даст представление о различных факторах, которые следует учитывать при создании API для обслуживания моделей.

Прежде чем мы перейдем к нашим ресурсам, давайте сначала кратко рассмотрим API фиктивной модели, который я создал для поддержки этой статьи. И если вы хотите следовать моему коду, пожалуйста, найдите его здесь, на GitHub.

Обзор API

Для этого проекта я создал очень простую модель логистической регрессии на основе популярного общедоступного набора данных Iris. Поскольку меня не обязательно интересует создание причудливой модели прогнозирования для нашего проекта, вы обнаружите, что я не занимался ни разработкой специальных функций, ни настройкой гиперпараметров. В моем обучающем сценарии ниже вы можете видеть, что единственное, что я делаю, - это загрузка набора данных с помощью Scikit-Learn, отделение целевого значения y от набора данных, передача обучающего набора данных в модель Scikit-Learn LogisticRegression и сохранение сериализованной модели в файл рассола с именем iris_model.pkl.

# Importing the required Python libraries
import numpy as np
import pandas as pd
import pickle
from sklearn import datasets
from sklearn.linear_model import LogisticRegression
# Loading the iris dataset from Scikit-Learn
iris = datasets.load_iris()
# Converting the iris dataset into a Pandas DataFrame
df_iris = pd.DataFrame(data = np.c_[iris['data'], iris['target']],
            columns = iris['feature_names'] + ['target'])
# Separating the training dataset (X) from the predictor value (y)
X = df_iris.drop(columns = ['target'])
y = df_iris[['target']]
# Instantiating a Logistic Regression (LR) model
lr_model = LogisticRegression()
# Fitting the dataset to the LR model
lr_model.fit(X, y)
# Saving the model to a serialized .pkl file
pkl_filename = "../models/iris_model.pkl"
with open(pkl_filename, 'wb') as file:
    pickle.dump(lr_model, file)

Создав модель, я готов к созданию API. В рамках приведенного ниже сценария вы можете видеть, что я использую инфраструктуру FastAPI для создания API обслуживания модели с двумя конечными точками. Первая конечная точка по умолчанию просто возвращает сообщение JSON, в котором указано Hello friends!. Вторая конечная точка, /predict, фактически принимает данные и возвращает результат логического вывода из нашей модели. Опять же, здесь мы делаем очень простые вещи. Скорее всего, ваш настоящий API будет намного сложнее, чем этот, но мы сохраняем простоту для нашей публикации здесь.

import pandas as pd
import pickle
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
## API INSTANTIATION
## ----------------------------------------------------------------
# Instantiating FastAPI
api = FastAPI()
# Loading in model from serialized .pkl file
pkl_filename = "../models/iris_model.pkl"
with open(pkl_filename, 'rb') as file:
    lr_model = pickle.load(file)
## API ENDPOINTS
## ----------------------------------------------------------------
# Defining a test root path and message
@api.get('/')
def root():
    msg = {'message': 'Hello friends!'}
    return JSONResponse(content = msg, status_code = 200)
# Defining the prediction endpoint
@api.post('/predict')
async def predict(request: Request):
    # Getting the JSON from the body of the request
    input_data = await request.json()
    # Converting JSON to Pandas DataFrame
    input_df = pd.DataFrame([input_data])
    # Getting the prediction from the Logistic Regression model
    pred = lr_model.predict(input_df)[0]
return JSONResponse(content = pred, status_code = 200)

Наконец, на случай, если вы захотите протестировать этот API самостоятельно, я создал несколько примеров данных JSON, которые отлично подойдут для этих целей. Вот как это выглядит:

{"sepal_length":5.1,"sepal_width":3.5,"petal_length":1.4,"petal_width":0.2}

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

1. Модульное тестирование с помощью Pytest

Номер один не зря, это, вероятно, тот, с которым вы лучше всего знакомы. Даже если вы не привыкли к усилению защиты API, вы, скорее всего, создали модульный тест для собственной проверки работоспособности. Для наших целей мы собираемся использовать pytest и тестовый клиент FastAPI. Установить pytest очень просто. Все, что вам нужно сделать, это выполнить следующую команду:

pip3 install pytest

Теперь, чтобы реально использовать pytest в интерфейсе командной строки, мы запустим крошечный сценарий bash, который выполняет наш тестовый файл и записывает результат в каталог reports. Вот как это выглядит:

pytest --log-file=reports/unit_test_report.txt unit_testing/

Хорошо, это просто ... Но как он на самом деле знает, что тестировать? Обратите внимание в конце этой команды выше, что мы pytest прочесываем все файлы в каталоге unit_testing. В моем каталоге unit_testing у меня есть один скрипт Python, который одновременно настраивает клиент модульного тестирования и запускает модульные тесты. Вот как выглядит этот полный сценарий:

import json
from fastapi.testclient import TestClient
from container.api import api
## PYTEST SETUP
## --------------------------------------------------------------------------------------------------------------------
# Instantiating the test client from our container's API
client = TestClient(api)
# Loading test JSON file
with open('test_json/test_data.json', 'rb') as file:
    test_json = json.load(file)
## UNIT TEST CASES
## --------------------------------------------------------------------------------------------------------------------
# Creating a unit test for the basic root path
def test_root_message():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {'message': 'Hello friends!'}
# Creating a unit test for the prediction endpoint
def test_predict():
    response = client.post('/predict', json = test_json)
    assert response.status_code == 200

Здесь вы заметите две отдельные секции. В разделе «Настройка Pytest» мы фактически создаем экземпляр тестового клиента FastAPI, используя тот же сценарий FastAPI, который мы создали ранее, а затем также загружаем этот небольшой фрагмент данных образца. Во втором разделе мы используем как тестовый клиент FastAPI, так и образцы данных для запуска некоторых тестовых случаев с операторами assert. Обратите внимание, что эти assert операторы имеют определенные условия, которые необходимо выполнить, чтобы пройти модульное тестирование. Если какой-либо из этих тестовых примеров не прошел, Pytest соответствующим образом отметит это вместе с кратким объяснением того, что не удалось. На скриншоте ниже показано, что происходит, когда я намеренно меняю внешний вид модульного теста на «Привет, друг!» вместо соответствующего «Привет, друзья!»

Ранняя настройка модульных тестов - это замечательно, потому что вы можете настроить конвейер CI / CD, который будет запускать этот тест каждый раз, когда вы вносите изменения в свой API. Если вы допустили ошибку, модульный тест обнаружит ее и сообщит вам о проблеме. Модульные тесты определенно спасли меня от множества ошибок по невнимательности, так что обязательно пишите их для своего API обслуживания моделей!

2. Тестирование производительности с помощью Locust

Поскольку API-интерфейсы часто вызываются тысячи, если не миллионы раз в день, вы обязательно захотите убедиться, что API, обслуживающий модель, может должным образом справляться с нагрузкой. Для этого у нас есть инструмент под названием Locust, который нам поможет. Я не собираюсь тратить на это много времени, поскольку я написал полный пост, в котором более подробно рассказывается о Locust. Я определенно рекомендую вам проверить это, если вы хотите узнать больше. Единственное, что я здесь отмечу, это то, что статья посвящена использованию Locust в модели пользовательского интерфейса. Вполне возможно запустить его без пользовательского интерфейса в так называемом безголовом режиме. Вот как выглядит эта команда:

locust — locustfile performance_testing/locustfile.py — headless — users 15 — spawn-rate 5 — run-time 30s — only-summary — csv reports/performance_test

При запуске этой команды Locust выполнит тест производительности, а затем запишет результаты как часть 4 отдельных файлов CSV. Честно говоря, мне хотелось бы, чтобы мы могли экспортировать его как единый отчет, но я полагаю, что что-то лучше, чем ничего! Опять же, мы не собираемся подробно останавливаться на том, как работает эта команда Locust выше, поэтому я определенно рекомендую вам прочитать мой предыдущий пост, если вы хотите узнать больше.

3. Безопасное сканирование зависимостей

Этот и следующий идут рука об руку, но мы начнем с этого, поскольку он «менее надежен» из двух. При контейнеризации API обслуживания моделей вам обязательно нужно выполнить pip install установку всех библиотек Python, необходимых для поддержки вашей работы. При сканировании зависимостей просматриваются все закрепленные версии ваших зависимостей в requirements.txt файле и вы узнаете, какие уязвимости связаны с соответствующими библиотеками Python.

Для наших целей мы воспользуемся бесплатным инструментом под названием Безопасность. Безопасность очень проста и удобна в использовании. Для установки безопасности все, что вам нужно сделать, это выполнить следующую команду:

pip install safety

На самом деле выполнить команду безопасности так же просто. В приведенной ниже команде вы увидите, что она указывает прямо на мой requirements.txt файл, а затем соответствующим образом сохраняет полный отчет.

safety check -r ../dependencies/requirements.txt --full-report -o reports/dependency_scan_report.txt

Если вы не в курсе, существует база данных с открытым исходным кодом, поддерживаемая Министерством внутренней безопасности США, под названием CVE. База данных CVE содержит полный спектр известных и распространенных уязвимостей безопасности, включая некоторые варианты того, как исправить соответствующую CVE. Теперь я старательно закрепляю свои зависимости с некоторыми из самых современных библиотек Python, но просто чтобы продемонстрировать, как работает безопасность, когда она находит что-то интересное, давайте посмотрим, что произойдет, когда я сканирую файл requirements.txt с помощью действительно старой версии Увикорн. (Uvicorn 0.0.4, если быть точным):

Как видите, служба безопасности отметила две проблемы с этой старой версией Uvicorn. Он дает хорошее описание для обоих, а также включает правильный номер CVE. Если вы хотите узнать больше об уязвимости, вы можете выполнить поиск, используя этот номер CVE. Например, первая уязвимость отмечает CVE-2020–7694. Если вы перейдете по этой ссылке, вы сможете узнать больше об этой уязвимости помимо относительно короткого описания, предоставленного Safety.

Так что это безопасность, но прежде чем двигаться дальше, я считаю, что мне следует упомянуть кое-что о важности решения от поставщика по сравнению с безопасностью, особенно если вы работаете в большой корпорации. Запросы безопасности ТОЛЬКО к государственной базе данных CVE, которая не всегда обновляется с учетом новейших данных. Решения от поставщиков часто содержат собственную базу данных с этими дополнительными проблемами уязвимости, поэтому, если ваша компания особенно озабочена безопасностью, я могу склонить вас к одному из этих решений.

4. Сканирование контейнеров с помощью Docker (Snyk)

Сканирование зависимостей - это замечательно, но проблема в том, что если вы собираетесь пойти по пути контейнеризации (а вы, вероятно, сделаете это), оно не получит всего, что связано с используемым вами базовым образом. Совсем недавно Docker заключил партнерство со сторонней компанией под названием Snyk, чтобы встроить инструмент сканирования контейнеров прямо в интерфейс командной строки Docker. Он очень прост в использовании и автоматически идет в комплекте с последними версиями Docker. Единственная загвоздка в том, что вам нужна учетная запись Docker Hub, но ее также можно создать бесплатно. Если вы не сделали этого раньше, вы можете создать учетную запись Docker Hub здесь.

Чтобы использовать сканирование контейнеров локально, вам нужно сделать две вещи. Сначала вам нужно войти в Docker Hub. Для этого выполните следующую команду:

docker login

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

docker build -t iris-api:dev .

Создав контейнер и выполнив вход в Docker, мы готовы выполнить сканирование контейнера! Вот как выглядит команда:

docker scan iris-api:dev | tee reports/container_scan_results.txt

Теперь вам может быть интересно, что tee здесь делает. Все другие наши инструменты предоставляют какой-то механизм для вывода результатов, но команда docker scan не имеет встроенной опции для экспорта результатов. Вместо этого вывод отображается только непосредственно в интерфейсе командной строки. Что tee позволяет нам сделать, так это получить этот вывод из интерфейса командной строки и сохранить его в текстовый файл. Если вы хотите использовать tee в других целях, вы вполне можете. Это никоим образом не ограничивается docker scan. Вот как выглядит результат моего сканирования:

Как видите, он поднял довольно много уязвимостей, определенно больше, чем наше сканирование зависимостей. Это потому, что я использую python:3.9-slim-buster, и Snyk обнаружил множество уязвимостей, связанных с ОС Debian, лежащей в основе этого образа. Скажу честно: я не знаю, согласен ли я со всеми этими выводами, поэтому используйте их с осторожностью. Я думаю, что Snyk - новая компания, поэтому я буду оптимистичен, что они доработают свои процессы, чтобы правильно отражать соответствующие уязвимости!

5. Статическое сканирование кода с помощью Bandit

Наше последнее сканирование безопасности в списке рассматривает потенциальные уязвимости вашего кода Python в целом, и мы делаем это с помощью инструмента под названием Bandit. Для этого сканирования мы укажем на каталог, содержащий все наши файлы Python, и укажем на то, что нам следует быть осторожными с точки зрения чистого Python. В моем случае все мои файлы Python (включая файл с FastAPI) находятся в каталоге с именем container, поэтому мы будем сканировать Bandit с помощью следующей команды:

bandit --format=txt --output=reports/static_scan_report.txt -r ../container/

В нашем сценарии код Python довольно прост, поэтому сканирование практически не проявляется. Если вам интересно, вот как выглядит результат моего конкретного сканирования:

Как видите, он справедливо отмечает, что мне следует с осторожностью относиться к импорту файла pickle, поскольку нередко кто-то упаковывает вредоносное ПО как часть файла pickle. В нашем случае мы знаем, что создали файл pickle, поэтому можем посмотреть на него и сказать: «Спасибо, Bandit, но я в порядке!»

6. Код ЛИНТЕР с Pylint

Этот последний ... я скажу необязательный. (Вы поймете, почему через минуту.) Если вы вспомните дни, когда вы учились в колледже, вы можете вспомнить, что ваш профессор заставлял вас писать статьи, используя специальный формат, такой как форматы MLA или APA. Если вы не в курсе, есть нечто очень похожее для мира программирования под названием PEP-8. Pylint специально проверяет ваш код Python, чтобы убедиться, что он соответствует стандартам PEP-8, среди прочего. Установить Pylint очень просто. Просто запустите следующую команду:

pip install pylint

И чтобы запустить линтер для вашего кода, все, что вам нужно сделать, это использовать pylint следующим образом:

pylint ../container/ --reports=y --output=reports/linter_report.txt

Вывод этой команды предоставит полное описание того, как вы могли бы улучшить, а также предоставит… * содрогается *… оценку того, насколько хорошо вы справились, из 10. (Чем выше, тем лучше.) Теперь давайте посмотрим, каковы мои результаты:

Ага, вы правильно прочитали. Я получил 3.00 из 10. Позвольте мне немного разглагольствовать… неужели вы, дорогой читатель, думаете, что мой код такой дерьмовый? Я чрезвычайно известен тем, что предоставляю подробные комментарии и разбиваю вещи на «разделы», поэтому я не понимаю, что получаю жалкие 3,00 из 10. Но, эй, я также боролся с профессорами по форматированию APA в колледже, так что это в моей природе - идти против течения в этих качествах. 😂 Тем не менее, я чувствовал, что стоит позвонить, если ваша организация действительно обеспокоена соблюдением рекомендаций PEP-8.

На этом этот пост завершен! Приятно вернуться. Надеюсь, вам понравился этот фильм, и ожидаем, что в ближайшем будущем появится еще больше интересного контента. Сейчас у меня есть сообщения о SageMaker, Terraform и многом другом. Будет весело! Спасибо за чтение!