Блог Amazon Web Services

Новая функциональность AWS Lambda: поддержка образов контейнеров

Оригинал статьи: ссылка (Danilo Poccia, Chief Evangelist (EMEA))

С помощью AWS Lambda вы можете загружать и запускать исходный код, не думая о серверах. Такой подход нравится многим нашим клиентам, но, если вы уже организовали процесс разработки с использованием контейнеров, его использование для запуска приложений в Lambda может быть затруднительно.

Для решения этой проблемы мы выпустили новую функциональность, которая позволяет упаковывать и разворачивать Lambda-функции в виде образов контейнеров размером до 10 ГБ. Благодаря этому, вы можете достаточно легко собирать и разворачивать более крупные приложения, которым требуется большое количество зависимостей, например, рабочие нагрузки, связанные с машинным обучением или работающие с большими объёмами данных. Функции, развёрнутые с помощью контейнеров, просты в использовании и поддерживают автоматическое масштабирование, высокую доступность и встроенную интеграцию со многими сервисами AWS так же, как и функции, запакованные в ZIP-архивы.

Мы предоставляем базовые образы для всех сред запуска, поддерживаемых в Lambda (Python, Node.js, Java, .NET, Go, Ruby): вам остаётся только добавить свой код и необходимые зависимости. Кроме того, мы предоставляем базовые образы для нестандартных сред запуска, которые основаны на Amazon Linux. С их помощью вы можете добавить свою среду запуска путём использования Lambda Runtime API.

Вы также можете запускать в Lambda и другие базовые образы – на основе Alpine или Debian Linux, что потребует реализовать для них Lambda Runtime API. Для облегчения задачи создания новых базовых образов мы выпустили Lambda Runtime Interface Clients, которые реализуют Runtime API для всех поддерживаемых сред запуска. Клиенты доступны через стандартные менеджеры пакетов, чтобы упростить их использование в пользовательских образах, и распространяются по лицензии с открытым исходным кодом.

Мы также выпустили Lambda Runtime Interface Emulator с открытым исходным кодом, который позволяет вам протестировать образ локально и убедиться, что он будет работать корректно при развёртывании в Lambda. Lambda Runtime Interface Emulator включён во все базовые образы, поставляемые AWS, а также может быть использован и в других образах.

Кроме того, ваши контейнерные образы могут использовать Lambda Extensions API для интеграции с системами мониторинга и безопасности и другими утилитами.

Для развёртывания образа контейнера используйте репозиторий Amazon Elastic Container Registry. Давайте посмотрим, как это работает на практике на нескольких примерах. В первом мы будем использовать образ для Node.js, предоставляемый AWS, а во втором – соберём свой собственный образ для Python.

Использование базового образа для Node.js, предоставляемого AWS

Ниже представлен исходный код простой Lambda-функции на Node.js (app.js), которая генерирует PDF-файл с помощью модуля PDFKit. При каждом вызове она создаёт новое письмо со случайными данными, сгенерированными через модуль faker.js. В выводе функции используется синтаксис Amazon API Gateway для возврата PDF-файла.

const PDFDocument = require('pdfkit');
const faker = require('faker');
const getStream = require('get-stream');

exports.lambdaHandler = async (event) => {

    const doc = new PDFDocument();

    const randomName = faker.name.findName();

    doc.text(randomName, { align: 'right' });
    doc.text(faker.address.streetAddress(), { align: 'right' });
    doc.text(faker.address.secondaryAddress(), { align: 'right' });
    doc.text(faker.address.zipCode() + ' ' + faker.address.city(), { align: 'right' });
    doc.moveDown();
    doc.text('Dear ' + randomName + ',');
    doc.moveDown();
    for(let i = 0; i < 3; i++) {
        doc.text(faker.lorem.paragraph());
        doc.moveDown();
    }
    doc.text(faker.name.findName(), { align: 'right' });
    doc.end();

    pdfBuffer = await getStream.buffer(doc);
    pdfBase64 = pdfBuffer.toString('base64');

    const response = {
        statusCode: 200,
        headers: {
            'Content-Length': Buffer.byteLength(pdfBase64),
            'Content-Type': 'application/pdf',
            'Content-disposition': 'attachment;filename=test.pdf'
        },
        isBase64Encoded: true,
        body: pdfBase64
    };
    return response;
};

Я использую npm для инициализации пакета и добавления трёх необходимых зависимостей в файл package.json. Также я создаю файл package-lock.json, который включу в образ контейнера для более предсказуемого результата.

$ npm init
$ npm install pdfkit
$ npm install faker
$ npm install get-stream

Теперь я создаю Dockerfile для образа контейнера моей Lambda-функции. В качестве базового образа я использую образ для среды запуска nodejs12.x, предоставляемый AWS. Все базовые образы от AWS доступны в Docker Hub и ECR Public. В данном случае я использую Docker Hub:

FROM amazon/aws-lambda-nodejs:12
COPY app.js package*.json ./
RUN npm install
CMD [ "app.lambdaHandler" ]

Для использования образа из ECR Public достаточно заменить первую строку файла на следующую:

FROM public.ecr.aws/lambda/nodejs:12

В приведённом выше Dockerfile в базовый образ добавляется исходный код (app.js) и файлы, описывающие сам пакет и его зависимости (package.json и package-lock.json). Затем для установки зависимостей запускается npm. С помощью инструкции CMD я задаю обработчик функции, но это можно сделать и позже – при конфигурации Lambda-функции.

Для локальной сборки образа random-letter я использую Docker CLI:

$ docker build -t random-letter .

Чтобы проверить, что всё работает корректно, я запускаю контейнер локально с использованием Lambda Runtime Interface Emulator:

$ docker run -p 9000:8080 random-letter:latest

Теперь протестируем вызов функции с помощью cURL. Я передаю пустой документ JSON в качестве входных данных:

$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'

При возникновении ошибок я могу исправить их локально. Если всё работает, переходим к следующему шагу.

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

$ aws ecr create-repository --repository-name random-letter --image-scanning-configuration scanOnPush=true
$ docker tag random-letter:latest 123412341234.dkr.ecr.sa-east-1.amazonaws.com/random-letter:latest
$ aws ecr get-login-password | docker login --username AWS --password-stdin 123412341234.dkr.ecr.sa-east-1.amazonaws.com
$ docker push 123412341234.dkr.ecr.sa-east-1.amazonaws.com/random-letter:latest

Для создания функции я использую Консоль управления AWS, но вы также можете использовать AWS Serverless Application Model (SAM), которая теперь тоже поддерживает образы контейнеров.

В консоли Lambda я нажимаю Create function. На открывшейся странице я выбираю опцию Container image, задаю название функции, а затем нажимаю кнопку Browse images, чтобы найти необходимый образ в моих репозиториях ECR.

Скриншот консоли AWS: создание новой Lambda-функции

После выбора репозитория я выбираю загруженный ранее образ с тегом latest. После того как я выбрал необходимый образ, Lambda запоминает его digest (на изображении ниже он отображается справа от тега). Вы можете увидеть digest для локальных образов с помощью команды docker images --digests. Благодаря такому подходу функция будет всегда использовать одинаковый образ, даже если тег latest будет переназначен, что позволяет защититься от непреднамеренных изменений. Вы также можете изменить образ, используемый в функции. При этом изменение конфигурации функции не приводит к изменению образа, даже если тег к тому времени был переназначен.

Скриншот консоли AWS: выбор образа контейнера

При желании я могу переопределить некоторые значения образа контейнера. Сейчас я не буду этого делать, но это позволяет использовать один образ для нескольких функций путём переопределения обработчика в значении поля CMD.

Скриншот консоли AWS: переопределение значений

Я оставляю стандартные значения в остальных полях и нажимаю Create function.

При создании или обновлении кода функции платформа Lambda оптимизирует образы контейнеров, подготавливая их к получению вызовов. Такая оптимизация занимает от нескольких секунд до нескольких минут в зависимости от размера образа. После её завершения я могу вызвать функцию и протестировать её в консоли.

Скриншот консоли AWS: результат тестирования Lambda-функции

Работает! Давайте теперь добавим API Gateway в качестве триггера. Я нажимаю Add Trigger и создаю API Gateway с использованием HTTP API. Для простоты я оставляю аутентификацию в API открытой для всех.

Скриншот консоли AWS: добавление триггера к Lambda-функции

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

Скриншот консоли AWS: API Gateway в качестве триггера для Lambda-функции

Всё работает корректно! Ниже представлено несколько примеров PDF-файлов со случайными данными из модуля faker.js.

Результаты работы тестового приложения

Создание нестандартного образа для Python

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

В данном случае я хочу создать образ с использованием Python 3.9. Исходный код моей функции (app.py) очень простой, я хочу просто поздороваться и вывести используемую версию Python.

import sys
def handler(event, context): 
    return 'Hello from AWS Lambda using Python' + sys.version + '!'

Как я уже упоминал, Lambda Runtime Interface Clients (которые реализуют Runtime API) доступны с открытым исходным кодом для всех поддерживаемых сред запуска. Я начну с образа Python, основанного на Alpine Linux, а затем добавлю к нему Lambda Runtime Interface Client для Python. Вот так выглядит Dockerfile:

# Define global args
ARG FUNCTION_DIR="/home/app/"
ARG RUNTIME_VERSION="3.9"
ARG DISTRO_VERSION="3.12"

# Stage 1 - bundle base image + runtime
# Grab a fresh copy of the image and install GCC
FROM python:${RUNTIME_VERSION}-alpine${DISTRO_VERSION} AS python-alpine
# Install GCC (Alpine uses musl but we compile and link dependencies with GCC)
RUN apk add --no-cache \
    libstdc++

# Stage 2 - build function and dependencies
FROM python-alpine AS build-image
# Install aws-lambda-cpp build dependencies
RUN apk add --no-cache \
    build-base \
    libtool \
    autoconf \
    automake \
    libexecinfo-dev \
    make \
    cmake \
    libcurl
# Include global args in this stage of the build
ARG FUNCTION_DIR
ARG RUNTIME_VERSION
# Create function directory
RUN mkdir -p ${FUNCTION_DIR}
# Copy handler function
COPY app/* ${FUNCTION_DIR}
# Optional – Install the function's dependencies
# RUN python${RUNTIME_VERSION} -m pip install -r requirements.txt --target ${FUNCTION_DIR}
# Install Lambda Runtime Interface Client for Python
RUN python${RUNTIME_VERSION} -m pip install awslambdaric --target ${FUNCTION_DIR}

# Stage 3 - final runtime image
# Grab a fresh copy of the Python image
FROM python-alpine
# Include global arg in this stage of the build
ARG FUNCTION_DIR
# Set working directory to function root directory
WORKDIR ${FUNCTION_DIR}
# Copy in the built dependencies
COPY --from=build-image ${FUNCTION_DIR} ${FUNCTION_DIR}
# (Optional) Add Lambda Runtime Interface Emulator and use a script in the ENTRYPOINT for simpler local runs
ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/bin/aws-lambda-rie
RUN chmod 755 /usr/bin/aws-lambda-rie
COPY entry.sh /
ENTRYPOINT [ "/entry.sh" ]
CMD [ "app.handler" ]

На этот раз Dockerfile гораздо более подробный. Он создаёт образ в три этапа, следуя лучшей практике Docker для многоступенчатой сборки. Вы можете применять такой же подход с тремя этапами для создания своих образов:

  • На первом этапе происходит сборка базового образа со средой запуска, в данном случае Python 3.9, а также устанавливается GCC, который позже понадобится для сборки зависимостей.
  • На втором этапе устанавливается Lambda Runtime Interface Client, а также копируется сама функция и устанавливаются её зависимости.
  • На третьем этапе создаётся финальный образ, который добавляет результат второго этапа к базовому образу из первого этапа. Здесь я также добавил Lambda Runtime Interface Emulator, но это необязательный шаг (больше информации об этом ниже).

Я создал скрипт entry.sh для использования в качестве ENTRYPOINT. Он запускает Lambda Runtime Interface Client для Python. При локальном запуске Runtime Interface Client вызывается через Lambda Runtime Interface Emulator.

#!/bin/sh
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
    exec /usr/bin/aws-lambda-rie /usr/local/bin/python -m awslambdaric $1
else
    exec /usr/local/bin/python -m awslambdaric $1
fi

Теперь я могу использовать Lambda Runtime Interface Emulator для локальной проверки, что функция и контейнер работают корректно:

$ docker run -p 9000:8080 lambda/python:3.9-alpine3.12

Образ контейнера без Lambda Runtime Interface Emulator

В образ контейнера необязательно добавлять Lambda Runtime Interface Emulator. Если я не включу его, я всё равно могу тестировать локально путём установки Lambda Runtime Interface Emulator на мой локальный компьютер с помощью следующих шагов:

  • Я удаляю команды копирования Lambda Runtime Interface Emulator (aws-lambda-rie) и скрипта entry.sh из третьего этапа Dockerfile, в данном случае они нам не понадобятся.
  • Я использую следующий ENTRYPOINT для запуска Lambda Runtime Interface Client по умолчанию:
    ENTRYPOINT [ "/usr/local/bin/python", “-m”, “awslambdaric” ]
  • Я использую следующие команды ля установки Lambda Runtime Interface Emulator на локальный компьютер, например, в директорию ~/.aws-lambda-rie:
mkdir -p ~/.aws-lambda-rie
curl -Lo ~/.aws-lambda-rie/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie
chmod +x ~/.aws-lambda-rie/aws-lambda-rie

После того как Lambda Runtime Interface Emulator установлен локально, я могу подключить его при запуске контейнера. Теперь команда для локального запуска будет выглядеть следующим образом (если Lambda Runtime Interface Emulator установлен в директории ~/.aws-lambda-rie):

docker run -d -v ~/.aws-lambda-rie:/aws-lambda -p 9000:8080 \
       --entrypoint /aws-lambda/aws-lambda-rie lambda/python:3.9-alpine3.12
       /lambda-entrypoint.sh app.handler

Тестирование нестандартного образа для Python

Независимо от того каким из двух способов я запустил контейнер локально, я могу протестировать вызов функции с помощью cURL:

curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'

В результате я получаю то, что и ожидал:

"Hello from AWS Lambda using Python3.9.0 (default, Oct 22 2020, 05:03:39) \n[GCC 9.3.0]!"

Затем я загружаю образ в ECR и создаю функцию так же, как и в первом примере. Вот результат теста моей функции в консоли:

Полученный образ основан на Alpine и использует Python 3.9 в Lambda.

Функциональность уже доступна для использования

Вы можете использовать образы контейнеров в ваших Lambda-функциях уже сегодня в регионах US East (N. Virginia), US East (Ohio), US West (Oregon), Asia Pacific (Tokyo), Asia Pacific (Singapore), Europe (Ireland), Europe (Frankfurt), South America (São Paulo). Мы планируем выпустить поддержку дополнительных регионов в ближайшее время. Поддержка образов контейнеров доступна в дополнение к поддержке функций в ZIP-архивах. Мы планируем поддерживать оба формата упаковки Lambda-функций.

Использование этой функции не требует дополнительных затрат. Вы платите за использование репозитория ECR, а также стандартную цену на Lambda.

Вы можете воспользоваться новой функциональностью по поддержке образов контейнеров в AWS Lambda через консольИнтерфейс командной строки AWS (CLI)AWS SDKAWS Serverless Application Model (SAM)AWS Cloud Development Kit (CDK), а также решений от партнёров AWS, включая Aqua SecurityDatadogEpsagonHashiCorp TerraformHoneycombLumigoPulumiStackerySumo Logic и Thundra.

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

Узнайте больше и начните использовать образы контейнеров с AWS Lambda.