Pipenv + AWS Lambda

Суть

1
$ pipenv run pip install -r <(pipenv lock -r) --target _build/

Взято отсюда.

Пояснения

Виртуальное окружение

Я сейчас много работаю с компонентами AWS и с AWS Lambda в частности. Писать код для лямбд можно на нескольких языках — на текущий момент поддерживаются Node.js, Java, Python и C#. Мы в команде используем пайтон. Так как до сих пор в ходу две его основных версии и для разных проектов могут требоваться различные версии библиотек, для управления версиями самого языка и пакетов принято использовать виртуальное окружение (virtual environment).

Традиционно виртуальное окружение создавалось следующим образом:

1
2
$ python3 -m venv _env
$ source _env/bin/activate

Первая команда создаёт чистый дистрибутив с пайтон 3 в локальной директории _env. Вторая команда активирует виртуальное окружение — изменяет переменную окружения PATH, добавляет VIRTUAL_ENV и стирает PYTHONHOME.

После этого команды pip, pip3, python и python3 будут работать в контексте дистрибутива в директории _env.

Деактивировать виртуальное окружение можно командой deactivate, которая вернёт переменным окружения изначальные значения.

Следующий шаг в настройке среды для разработки — это установка зависимостей. Если проект распространяется в виде пакета, то зависимости указываются в файле setup.py. Для проектов, которые не предполагается распространять или подключать как зависимости, используются файлы со списками зависимостей. Название файла может быть любым, но обычно это requirements.txt с зависимостями самого приложения и requirements-dev.txt с опциональными зависимостями для разработки.

1
2
# Установка зависимостей из файла
$ pip install -r requirements.txt

Деплой лямбды

Для того, чтобы загрузить код лямбды в AWS, его нужно упаковать в zip-архив со всеми зависимостями. Это удобно делать, если всё необходимое находится в одной директории. Порядок команд мы использовали следующий:

1
2
3
4
5
6
# В директории проекта
$ python3 -m venv _env
$ source _env/bin/activate
$ mkdir -p _build
$ pip install -r requirements.txt -t _build
$ cp -R src/* _build

После этого можно запаковать содержимое директории _build в архив и загрузить в AWS S3, либо использовать расширение AWS Serverless Application Model (SAM), которое автоматизирует и упрощает процесс. Мы используем последний вариант, но это отдельная тема.

И что не так?

У вышеописанного подхода есть один существенный недостаток и несколько несущественных.

Для указания версии зависимостей существует специальный стандарт PEP 440, который позволяет не фиксировать версию жёстко, а указать её в виде некоего правила. Обычно ограничиваются указанием версии, в рамках которой сохраняется обратная совместимость.

Например:

1
2
torchtext>=0.2.1
bs4PyMySQL>=0.7,<1.0

Проблема в том, что устанавливается последняя доступная версия, поэтому действительные версии библиотек могут отличаться для двух идентичных вызовов pip install.

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

Для решения этой проблемы менеджеры зависимостей других языков используют так называемые lock-файлы. Например, популярный менеджер для PHP — Composer — использует composer.lock, a NPM — для Node.js — использует package-lock.json. Эти файлы генерируются и обновляются автоматически при установке новых зависимостей или обновлении имеющихся и содержат информацию о конкретных версиях зависимостей. Это позволяет быть уверенным, что установка зависимостей из lock-файлов в любой момент даст идентичный результат. Дополнительно имеются данные для проверки целостности, что позволяет заметить, если версия та же, но код зависимости всё же отличается.

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

Ещё не очень удобно самостоятельно указывать зависимости в requirements.txt. Были случаи, когда разработчик устанавливал зависимость во время разработки, но забывал вписать её в файл, из-за чего у коллег не запускался проект.

Pipenv

Проект pipenv призван избавить разработчика от обозначенных проблем.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# создать виртуальное окружение
# (создаётся директория с дистрибутивом Python 3 и файл Pipfile)
$ pipenv --three

# установить зависимости
# (добавляются записи в секцию [[packages]] в Pipfile и генерируется/обновляется Pipfile.lock)
$ pipenv install 'pymysql>=0.7,<1' boto3

# установить зависимости для разработки
# (записи добавляются в секцию [[packages-dev]])
$ pipenv install -d rope

# обновить зависимости до последних версий
$ pipenv update

Если в проекте уже существует Pipfile, то настроить виртуальное окружение и установить зависимости можно одной командой:

1
$ pipenv install

Настроить окружение для разработки — тоже одной (при этом, предыдущую команду вызвать не требуется):

1
$ pipenv install -d

Если имеется Pipfile.lock, то будут установлены именно те версии, которые в нём указаны.

Pipenv + AWS Lambda

Утилита pipenv относительно новая, находится в активной разработке и некоторых возможностей она не предоставляет, а некоторые планируют и вовсе убрать (например).

Для сборки пакета для лямбды критически важно иметь возможность установить зависимости в указанную директорию. «Из коробки» pipenv такую функцию не поддерживает. Но, как всегда, есть ободные пути. Один из них — это небольшой скрипт, который копирует исходники зависимостей из директории репозитория виртуального окружения. Другой — это генерация файла requirements.txt и установка зависимостей по-старинке, с помощью традиционной утилиты pip.

Второй вариант умещается в однострочник (хотя это две команды, а не одна):

1
$ pipenv run pip install -r <(pipenv lock -r) --target _build/
  1. pipenv lock -r генерирует список зависимостей, совместимый с синтаксисом requirements.txt
  2. pip install --target _build -r file устанавливает зависимости из указанного файла в указанную директорию
  3. pipenv run необходима, чтобы запустить pip в виртуальном окружении

Пошаговая инструкция

Начальные требования: установленные homebrew, pipenv и python3 (желательно, но должно работать и с python2).

1
2
3
4
5
6
# Установка homebrew
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
# Установка python3
$ brew install python3
# Установка pipenv
$ pip install pipenv

Настройка окружения (в директории проекта):

1
2
3
4
5
6
# Создание виртуального окружения
$ pipenv --three
# Установка необходимых зависимостей приложения
$ pipenv install <required dependencies>
# Установка зависимостей необходимых в ходе разработки
$ pipenv install -d <required development dependencies>

Сборка и публикация кода для лямбды:

1
2
3
4
5
6
7
# Установка зависимостей в отдельную директорию сборки
$ pipenv run pip install -r <(pipenv lock -r) --target _build/
# Копирование кода проекта в директорию сборки
$ cp -R src/* _build
# Далее необходимо загрузить код любым удобным способом
# Мы используем подход "aws cloudformation package & deploy"
$ <run aws cloudformation package,deploy commands as usual>

Это всё можно упаковать в Makefile и запускать одной командой.

🏁