Вызов 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-pack
X],
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-pack
X,
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-pack
X], description: "A layer with slack notification", }); new lambda_nodejs.NodejsFunction(this, "Slack Notification", { description: "Send slack notification", runtime: lambda.Runtime.NODEJSCargo install wasm-pack
X, 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.
Хотя некоторые обходные пути необходимы для правильной работы, стоит подумать, когда это действительно полезно для вашего проекта.