wasmcloud app development

一、引言

本文主要介绍基于 wasmcloud 的分布式开发概念和流程。通过以 wasm 为最小单元的开发,充分适用于当前的分布式开发环境。引用官方文档的原文来说:

Congratulations! You’ve successfully created and run your first actor. Welcome to the world of boilerplate-free, simple distributed application development in the cloud, browser, and everywhere in between.

在了解和探索的过程中,主要参考官方文档进行了相关的示例程序编写和运行,用到的相关材料罗列如下:

说明 网址
官网开发指引 https://wasmcloud.dev/app-dev/
官方examples代码 https://github.com/wasmCloud/examples
官方providers列表 https://github.com/wasmCloud/capability-providers
官方interfaces列表 https://github.com/wasmCloud/interfaces
wasmcloud平台代码 https://github.com/wasmCloud/wasmcloud-otp
rust权威指南 https://zh.b-ok.cc/book/17931171/7b822e
elixir教程 https://pragprog.com/titles/passelixir/programmer-passport-elixir/
OTP教程 https://pragprog.com/titles/passotp/programmer-passport-otp/

二、基本概念梳理

在展开介绍 wasmcloud 的应用开发之前,我们需要熟悉其抽象出的几个概念。

无论是采用传统的单体应用开发模式,还是微服务开发模式,亦或是 wasmcloud,其解决的问题基本类似,只不过不同的模式下称呼不同而已。

一般而言,应用的开发都可以划分为以下几个部分:

  1. 纯业务逻辑。*如:商品功能,会员功能等。面向业务开发人员。*
  2. 中间件。*如:数据库中间件,缓存中间件,流量控制等。面向中间件开发人员,或平台开发人员。*
  3. 运行环境。*根据不同的技术栈,有不同的运行环境,属于基础设施层。比如采用容器部署的,一般运行在k8s里,这部分面向平台开发人员。*

在 wasmcloud 中具有相似的概念,这里列举如下

概念 面向人员 说明
actor 业务开发 编写业务逻辑的地方,如果涉及到非业务逻辑的公共能力(或中间件能力),通过引用interface的方式来获取能力。业务人员可使用rust或tinyGo来开发actor。
interface 中间件或平台开发 采用CDD模式开发,定义一些业务无关的公共能力接口,比如:keyvalue能力,但是不提供具体的实现。可以在 interface 中定义允许的操作(operations),以及各种结构体(structure)。使用 smithy IDL 进行 interface 的定义,使用 wash 工具来生成对应代码。
provider 中间件或平台开发 实现interface的能力,比如同样对于keyvalue功能,可以有redis实现,也可以有vault实现。并且可以用不同的技术栈来实现。可使用 rust 来进行 Provider 开发,官方未来会支持 Go。
link def 业务开发或部署人员 由于actor只是引用了具体的interface,并没有指定哪一个provider,所以在实际部署时,需要关联actor和provider。可以通过 wasmcloud shell(wash)命令行,或者平台进行操作。
wasm host 中间件或平台开发 作为整个wasmcloud生态的运行基石,用于协调actor,provider之间的分布式通信,同时也提供分布式调度能力。采用 Elixir/OTP 技术栈实现。

整体的关系图梳理如下:

wasmcloud app 的开发采用 CDD(Contract Driven Develop) 方式进行,其中 interface 就是契约的定义。

三、开发流程

如果我们需要实现一个全新的业务场景,需要进行以下几个步骤:

)

接下来以 kvcounter 为例,进一步说明各个环节。

## 示例目标:

开发一个 kvcounter 示例程序,当用户访问 http://localhost:8080 时,能够正常显示 counter,且每次访问 +1。

示例效果如下:

)

## 1、创建(或使用)interface

在本示例中,我们用到了 kv 功能,所以需要找到一个能够提供 keyvalue 功能的 interface。有两种方式,一是找到一个现成可用的interface,二是定义一个keyvalue 的 interface。

这里以定义一个新的 keyvalue 为例。

### 1.1 使用 wash 工具创建新的 interface

1
wash new interface keyvalue

1.2 编写 interface,即:smithy 文件

首先确定该接口提供的操作,然后是每个操作(operations)涉及到的结构体(structure)

这里摘除主要的operation定义,完整的定义可参考链接:

interfaces/keyvalue.smithy at main · wasmCloud/interfaces

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// key-value.smithy
// Definition of a key-value store and the 'wasmcloud:keyvalue' capability contract
//

// Tell the code generator how to reference symbols defined in this namespace
metadata package = [{
namespace: "org.wasmcloud.interface.keyvalue",
crate: "wasmcloud_interface_keyvalue",
py_module: "wasmcloud_interface_keyvalue",
doc: "Keyvalue: wasmcloud capability contract for key-value store",
}]

namespace org.wasmcloud.interface.keyvalue

use org.wasmcloud.model#wasmbus
use org.wasmcloud.model#rename
use org.wasmcloud.model#n
use org.wasmcloud.model#U32
use org.wasmcloud.model#I32

@wasmbus(
contractId: "wasmcloud:keyvalue",
providerReceive: true )
service KeyValue {
version: "0.1.1",
operations: [
Increment, Contains, Del, Get,
ListAdd, ListClear, ListDel, ListRange,
Set, , SetAdd, SetDel, SetIntersection, SetQuery, SetUnion, SetClear,
]
}

/// Gets a value for a specified key. If the key exists,
/// the return structure contains exists: true and the value,
/// otherwise the return structure contains exists == false.
@readonly
operation **Get** {
input: String,
output: GetResponse,
}

/// Sets the value of a key.
/// expires is an optional number of seconds before the value should be automatically deleted,
/// or 0 for no expiration.
operation **Set** {
input: SetRequest,
}

/// Deletes a key, returning true if the key was deleted
@rename([{lang:"Python", name:"delete"}])
operation **Del** {
input: String,
output: Boolean,
}

/// Increments a numeric value, returning the new value
operation **Increment** {
input: IncrementRequest,
output: I32
}

我们看到 smithy 文件只定义了相关的接口,并不涉及到具体的实现。这也是 wasmcloud app开发中比较重要的概念。

actor只依赖于具体的interface,而一个interface可以由多个provider来实现。

对于本示例,同样是 wasmcloud:keyvalue 的契约,我们可以用 redis 技术栈来实现一个 provider,也可以用 vault 来实现。实际上官方提供了两个版本 provider 的实现:

https://github.com/wasmCloud/capability-providers

)

### 1.3 生成代码,发布到对应的仓,方便actor开发人员使用

我们可以直接执行 make 指令,生成对应的代码。之后可以把 interface 发布到 crate.io。

Go语言同理,这样在实际开发 actor 时,就可直接引入使用。

## 2、开发 provider

根据官方的说法,provider的作用如下:

启动后,通过 stdio 和 host 进行交互,其主要作用包括:健康检查、通过 RPC 和 actor 交互等。

> Creating a capability provider involves creating a native executable. All capability provider executables have the same basic requirements:
>
> - Accept a Host Data structure from stdin immediately upon starting the executable. The host data is a base64 encoded JSON object with a trailing newline making it easy to pull from the stdin pipe.
> - Accept linkdef messages according to the RPC protocol
> - Communicate with actors via rpc messages defined by a capability contract
> - Respond to periodic health checks

)

### 2.1 使用 wash 新建 provider

通过 wash 指令,可以生成 provider 的模板代码

1
wash new provider kvredis

2.2 编写 provider

重点是基于 redis 实现 interface 中定义的各种 operations,完整代码见链接,核心代码框架摘录如下:

capability-providers/main.rs at main · wasmCloud/capability-providers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

/// Handle KeyValue methods that interact with redis
#[async_trait]
impl KeyValue for KvRedisProvider {
/// Increments a numeric value, returning the new value
#[instrument(level = "debug", skip(self, ctx, arg), fields(actor_id = ?ctx.actor, key = %arg.key))]
**async fn increment**(&self, ctx: &Context, arg: &IncrementRequest) -> RpcResult<i32> {
let mut cmd = redis::Cmd::incr(&arg.key, &arg.value);
let val: i32 = self.exec(ctx, &mut cmd).await?;
Ok(val)
}

/// Deletes a key, returning true if the key was deleted
#[instrument(level = "debug", skip(self, ctx, arg), fields(actor_id = ?ctx.actor, key = %arg.to_string()))]
**async fn del**<TS: ToString + ?Sized + Sync>(&self, ctx: &Context, arg: &TS) -> RpcResult<bool> {
let mut cmd = redis::Cmd::del(arg.to_string());
let val: i32 = self.exec(ctx, &mut cmd).await?;
Ok(val > 0)
}

/// Gets a value for a specified key. If the key exists,
/// the return structure contains exists: true and the value,
/// otherwise the return structure contains exists == false.
#[instrument(level = "debug", skip(self, ctx, arg), fields(actor_id = ?ctx.actor, key = %arg.to_string()))]
**async fn get**<TS: ToString + ?Sized + Sync>(
&self,
ctx: &Context,
arg: &TS,
) -> RpcResult<GetResponse> {
let mut cmd = redis::Cmd::get(arg.to_string());
let val: Option<String> = self.exec(ctx, &mut cmd).await?;
let resp = match val {
Some(s) => GetResponse {
exists: true,
value: s,
},
None => GetResponse {
exists: false,
..Default::default()
},
};
Ok(resp)
}

/// Sets the value of a key.
/// expires is an optional number of seconds before the value should be automatically deleted,
/// or 0 for no expiration.
#[instrument(level = "debug", skip(self, ctx, arg), fields(actor_id = ?ctx.actor, key = %arg.key))]
**async fn set**(&self, ctx: &Context, arg: &SetRequest) -> RpcResult<()> {
let mut cmd = match arg.expires {
0 => redis::Cmd::set(&arg.key, &arg.value),
_ => redis::Cmd::set_ex(&arg.key, &arg.value, arg.expires as usize),
};
let _value: Option<String> = self.exec(ctx, &mut cmd).await?;
Ok(())
}

除了基于 redis 实现 provider 外,官方还提供了基于 valut 的 keyvalue 实现,可参阅

capability-providers/main.rs at main · wasmCloud/capability-providers

其中也是有诸如 increment, del, set, get 的实现,而这些 operation 都是在名为 wasmcloud:keyvalue 的契约中定义的。

2.3 生成 provider 产物

简单的使用 make 指令即可完成部署产物,该产物可以推送至 OCI 仓库以方便使用。

3、开发 actor

3.1 新建 actor

使用 wash 指令可以很方便的生成便于快速开发的模板代码:

1
wash new actor kvcounter

3.2 开发 actor

声明契约依赖

kvcounter 依赖了两个 interface,用来提供两个能力:httpserver能力和 kv 存储能力。

我们需要声明该依赖,这样打包完成 kvcounter 后,可以 inspect 出对应的信息。

)

所以我们需要修改 makefile 的 CLAIMS 信息:

1
2
3
4
5
6
7
# examples/actor/kvcounter

PROJECT = kvcounter
VERSION = $(shell cargo metadata --no-deps --format-version 1 | jq -r '.packages[] .version' | head -1)
REVISION = 0
# list of all contract claims for actor signing (space-separated)
**CLAIMS = wasmcloud:httpserver wasmcloud:keyvalue**

使用 interface

根据不同语言,我们可以引入不同的 interface package。以 rust 为例,官方的 interface 可以在 crates.io 中查阅

)

我们可在 cargo.toml 中加入 kvcounter 所依赖的两个包:

1
2
3
4
5
6
[package]
name = "kvcounter"

[dependencies]
**wasmcloud-interface-keyvalue = "0.7.0"**
**wasmcloud-interface-httpserver = "0.6.0"**

在 actor 中调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#[async_trait]
impl HttpServer for KvCounterActor {
async fn handle_request(&self, ctx: &Context, req: &HttpRequest) -> RpcResult<HttpResponse> {
// increment the value in kv and send response in json
let (body, status_code) = match **increment_counter**(ctx, key, amount).await {
Ok(v) => (json!({ "counter": v }).to_string(), 200),
// if we caught an error, return it to client
Err(e) => (json!({ "error": e.to_string() }).to_string(), 500),
};
let resp = HttpResponse {
body: body.as_bytes().to_vec(),
status_code,
..Default::default()
};
Ok(resp)
}
}

/// increment the counter by the amount, returning the new value
async fn increment_counter(ctx: &Context, key: String, value: i32) -> RpcResult<i32> {
let new_val = KeyValueSender::new()
**.increment(ctx, &IncrementRequest { key, value })**
.await?;
Ok(new_val)
}

3.4 打包

我们可以使用 make 指令,将代码打包为 .wasm 文件,进而推送到 OCI 仓库。

4、部署

我们可以使用 wasmcloud shell(即 wash) 或者 web dashboard 进行部署。主要进行如下操作:

  1. start actors。将 actor 的 wasm 文件上传到 wasmcloud 运行时,或者提供 registry 地址供其下载。
  2. start providers。将 provider 的包上传到 wasmcloud 运行时,或提供 registry 地址供其下载。
  3. 建立 link 关系。

在平台的操作截图效果如下:

)

这里需要注意的是,kvcounter 的 actor 需要分别与 httpserver 以及 kvredis 的 provider 进行 link,也就是有两条 link 记录。

## 5、访问

至此,已能正常通过 http 进行访问。

1
2
3
4
➜  ~ curl localhost:8080
{"counter":17}%
➜ ~ curl localhost:8080
{"counter":18}%

NEXT

本文主要是梳理了 wasmcloud app 开发的概况,包括其主要涉及的概念、工具链的使用、主要的开发流程。

在当前的探索过程中,已基本熟悉 wasmcloud 的外围生态,能够根据实际的工作需求,进行部分平台数据获取(或UI)的修改,同时提交 PR,参与官方的平台建设。

下一步需要进一步探究 wasmcloud runtime 的具体实现,尤其是如何调度 actor, provider,以及 actor 和 provider 之间的协作通信细节。