Вызов 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содержит модули Rustlayersсодержит сгенерированные файлы wasmfunctionsсодержит функции Lambda, которые используют функции слоя Lambda.package.jsonдолжно содержатьworkspaces
"workspaces": [ "functions/*", "layers/*" ]
Инструкции по реализации
- Установите
wasm-packна свой локальный компьютер.Cargo install wasm-pack - Создайте модуль Rust, используя
Cargo new wasm-addв каталогеfn. Пишите код на Rust. - Выполнить
wasm-pack build -d ../../layers/wasm-add --target nodejs. Это создаст файлы wasm для формата nodejs. - Один трюк: внутри каталога
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.
Хотя некоторые обходные пути необходимы для правильной работы, стоит подумать, когда это действительно полезно для вашего проекта.