Вызов WebAssembly на основе Rust из слоя AWS Lambda

WebAssembly — это новая технология, позволяющая разработчикам писать код на языках, отличных от JavaScript, и запускать его в Интернете. Одним из популярных языков для написания WebAssembly является Rust, язык системного программирования, обеспечивающий безопасность памяти и высокую производительность.

Используя WebAssembly на основе Rust (wasm), разработчики могут получить доступ к производительности и безопасности Rust, сохраняя при этом доступ к функциям из TypeScript. Кроме того, мы можем не включать файлы wasm в Lambda, что уменьшает размер и делает его пригодным для повторного использования. В этом сообщении блога я объясню способ использования WebAssembly на основе Rust из Lambda Layers.

Если вы просто хотите перейти к коду, вот ссылка:



Испытание

При использовании aws_lambda_nodejs esbuild объединяет все файлы из слоя Lambda в один index.js. Хотя многие блоги и руководства рекомендуют использовать /opt/nodejs/{layerModule} для доступа к файлам в слое, это может значительно увеличить размер лямбда-функции.

Кроме того, нам нужно включить .wasm файлов в функцию Lambda. Причина этого в том, что файл JavaScript, соединяющий файл wasm, обращается к файлу wasm из того же каталога, используя __dirname. Поэтому включение файлов .wasm в функцию Lambda становится необходимым для обеспечения надлежащего доступа.

const path = require('path').join(__dirname, 'wasm_add_bg.wasm');
const bytes = require('fs').readFileSync(path);

Наша цель — не включать .wasm или JS-файлы моста в нашу функцию Lambda. Давайте посмотрим, как решить эту проблему.

Начиная с минимальной реализации

Предполагая, что проект основан на yarn berry, давайте сначала рассмотрим минимальную реализацию.

Структура каталогов

Структура каталогов должна выглядеть следующим образом:

.
├── bin
├── cdk.out
├── fn
│   └── wasm-add // Rust code
├── functions
│   └── lambda-function
├── layers
│   └── wasm-add
├── lib // where all CDK stack is located
│   ├── layer-stack.ts
│   └── cdk-stack.ts
└── package.json
  • fn содержит модули Rust
  • layers содержит сгенерированные файлы wasm
  • functions содержит функции Lambda, которые используют функции слоя Lambda.
  • package.json должно содержать workspaces
"workspaces": [
  "functions/*",
  "layers/*"
]

Инструкции по реализации

  1. Установите wasm-pack на свой локальный компьютер. Cargo install wasm-pack
  2. Создайте модуль Rust, используя Cargo new wasm-add в каталоге fn. Пишите код на Rust.
  3. Выполнить wasm-pack build -d ../../layers/wasm-add --target nodejs. Это создаст файлы wasm для формата nodejs.
  4. Один трюк: внутри каталога wasm-add скопируйте все сгенерированные файлы в nodejs/node_modules, используя rsync
rsync -av . --exclude='nodejs' ./nodejs/node_modules/

5. Создайте layer-stack.ts под lib. На слое Lambda файлы будут храниться под /opt/nodejs/node_modules/wasm-add. Функция Lambda имеет доступ к NODE_PATH, который включает /opt/nodejs/node_modules/Затем функция может получить доступ к файлу wasm, не связывая его с Lambda.

export class LayerStack extends Construct {
  public readonly layer: lambda.LayerVersion;
  constructor(scope: Construct, id: string) {
    super(scope, id);

    this.layer = new lambda.LayerVersion(this, "Layer", {
      code: lambda.Code.fromAsset(
        path.join(`${__dirname}/..`, "layers/wasm-add"),
      ),
      compatibleRuntimes: [lambda.Runtime.NODEJSCargo install wasm-packX],
      description: "A layer with wasm",
    });
  }
}

6. Настройте функцию Lambda в cdk-stack.ts. Опять же, обратите внимание, что мы исключаем wasm-add из этой лямбда-функции, поэтому мы не будем включать их в лямбда-функцию.

const layerStack = new LayerStack(this, "LayerStack");
new lambda_nodejs.NodejsFunction(this, "Add Function", {
  description: "Add two numbers",
  runtime: lambda.Runtime.NODEJSCargo install wasm-packX,
  handler: "handler",
  entry: path.join(
    `${__dirname}/../`,
    "functions",
    "simple-function/index.ts",
  ),
  layers: [layerStack.layer],
  bundling: {
     externalModules: [
         "aws-sdk", // Use the 'aws-sdk' available in the Lambda runtime
         "wasm-add", // exclude as it exists in lambda layer
     ],
   },
});

7. Разверните с помощью yarn cdk deploy! Вы можете видеть, что лямбда-функция не включает файлы wasm.

Более сложный сценарий

Теперь, когда у нас есть общее представление об использовании WebAssembly на основе Rust из Lambda Layers, давайте рассмотрим более сложный сценарий, в котором мы объединяем асинхронные вызовы между wasm и Lambda.

Мы будем реализовывать веб-перехватчик Slack, для которого требуется отправка запроса Post к Slack API. Асинхронные вызовы можно обрабатывать с помощью wasm-bindgen-futures. Вызовите Cargo add wasm-bindgen-futures, чтобы добавить его в зависимости.

Используйте JsValue для получения результата на стороне JS.

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub async fn send_slack_message(webhook_url: &str, message: &str) -> Result<JsValue, JsValue> {
    let payload = format!(r#"{}"#, message);
    let client = Client::new();
    let res = client
        .post(webhook_url)
        .header("Content-Type", "application/json")
        .body(payload)
        .send()
        .await
        .map_err(|e: Error| JsValue::from_str(&e.to_string()))?;
    if !res.status().is_success() {
        return Err(JsValue::from_str(&format!(
            "Slack API returned code: {} and Message:{}",
            res.status(),
            res.text().await.unwrap()
        )));
    }

    Ok(JsValue::from_str("Slack API Success"))
}

Скомпилируйте этот код Rust так же, как предыдущий пример. Вы увидите, что генерируется больше кода. Вы также заметите, что в сгенерированном коде используется fetch. wasm-pack генерирует файлы, требующие fetch полифиллов, как указано в https://rustwasm.github.io/wasm-pack/book/prerequisites/considerations.html.

Чтобы использовать этот JS-файл на Node.js, нам нужно включить node-fetch . Запустите yarn add node-fetch@2, поскольку на данный момент на Lambda Layer поддерживается только CommonJS. Затем замените fetch на node-fetch.

Вставьте приведенный ниже код в сгенерированный файл wasm js. В данном сценарии это slack_notif.js.

const fetch = require('node-fetch');
global.fetch = fetch;
global.Headers = fetch.Headers;
global.Request = fetch.Request;
global.Response = fetch.Response;

Теперь еще раз скопируйте все файлы под /layers/slack-notif/nodejs/node_modules

Лямбда-функция будет выглядеть так. Вы можете видеть, что мы просто импортируем «slack-notif», чтобы использовать эту функцию wasm.

import { send_slack_message } from "slack-notif";
const webhookUrl = process.env.SLACK_WEBHOOK_URL as string;
const composeMessage = ({
  title,
  message,
}: {
  title: string;
  message: string;
}) =>
...
  });

export const handler = async (event: any) => {
  const { message, title } = event;
  const result = await send_slack_message(
    webhookUrl,
    composeMessage({ title, message }),
  );
  console.log({ result });
  return {};
};

CDK будет выглядеть следующим образом:

slackLayer = new lambda.LayerVersion(this, "SlackLayer", {
      code: lambda.Code.fromAsset(
        path.join(`${__dirname}/..`, "layers/slack-notif"),
      ),
      compatibleRuntimes: [lambda.Runtime.NODEJSCargo install wasm-packX],
      description: "A layer with slack notification",
 });

new lambda_nodejs.NodejsFunction(this, "Slack Notification", {
      description: "Send slack notification",
      runtime: lambda.Runtime.NODEJSCargo install wasm-packX,
      handler: "handler",
      entry: path.join(`${__dirname}/../`, "functions", "slack/index.ts"),
      layers: [layerStack.slackLayer],
      environment: {
        SLACK_WEBHOOK_URL:
          // "https://hooks.slack.com/services/xxx/yyy/zzz",
          "Set your own webhook url",
      },
      bundling: {
        externalModules: [
          "aws-sdk", // Use the 'aws-sdk' available in the Lambda runtime
          "slack-notif", // make sure to exclude this module!! 
        ],
      },
 });

Давайте развернем и протестируем его!

Бонус: автоматизируйте процесс с помощью MakeFile

Я создал Makefile для автоматизации процесса генерации кода + вставил все необходимые импорты для полифилов fetch. Вы можете найти его здесь https://github.com/tomoima525/rust-wasm-lambda/blob/main/Makefile

Заключение

Использование WebAssembly на основе Rust из Lambda Layers — это мощный способ объединить производительность и безопасность Rust с простотой использования TypeScript.

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