极简istio wasmplugin go语言开发示例

前置条件

k8s集群,版本1.23.8

安装方法略

istio安装完成demo,版本1.17.2

curl -L https://git.io/getLatestIstio | ISTIO_VERSION=1.17.2 sh
cd istio-1.17.2/
./bin/istioctl install --set profile=demo -y
kubectl label namespace default istio-injection=enabled --overwrite

安装httpbin,sleep 示例

httpbin主要为了提供http响应,也可以用已有的能够提供http服务的任意服务。

sleep 为了在pod里使用curl命令,有别的pod中能执行curl命令也可以。

kubectl apply -f samples/httpbin/httpbin.yaml
kubectl apply -f samples/sleep/sleep.yaml

本地docker环境,版本 20.10.21

docker安装略

go语言环境,版本1.18.1

go安装略

阿里云或其他镜像仓库

类似部署一个普通应用,wasm插件来源于某个镜像仓库。

为了方便使用了阿里云的免费镜像仓库,也可以本地搭建或使用别的仓库。

注意需要将阿里云的仓库设置为公开!

示例功能

  • 在请求头增加自定义的返回信息

  • 将插件的配置信息放到头信息中返回(测试读取配置的功能)

  • 简单的限流功能

直接看效果

使用我编译好的镜像直接部署,保存以下内容为 gowasm.yaml

apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: my-go-wasm-plugin
  namespace: default
spec:
  selector:
    matchLabels:
      app: httpbin
  ## 编译好的镜像    
  url: oci://registry.cn-hangzhou.aliyuncs.com/hallywang/gowasm:20230530181612
  
  #插件的配置信息,在代码中可以获取到json string
  pluginConfig:
    testConfig: abcddeeeee
    listconfig:
     - abc
     - def

部署到k8s中

kubectl apply -f gowasm.yaml

示例验证方法

  • 执行以下命令,从sleep pod中发送http 请求到 httpbin ,打印出返回的header
SLEEP_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name}

kubectl exec ${SLEEP_POD} -c sleep -- sh -c 'for i in $(seq 1 3); do curl --head -s httpbin:8000/headers; sleep 0.1; done'

  • 在未部署gowasm.yaml之前,每次请求都会返回200成功,且头信息中没有自定义的内容

  • 在部署了gowasm.yaml之后,返回如下结果(两次请求的结果),有自定义的头信息和403的返回说明插件部署成功。

    ## 第一次请求的返回结果:
    HTTP/1.1 200 OK
    server: envoy
    date: Wed, 31 May 2023 03:20:12 GMT
    content-type: application/json
    content-length: 526
    access-control-allow-origin: *
    access-control-allow-credentials: true
    x-envoy-upstream-service-time: 3
    ## 下面是插件增加的头信息
    who-am-i: wasm-extension
    injected-by: istio-api!
    hally: wang
    wang: 1234567
    configdata: {"listconfig":["abc","def"],"testConfig":"abcddeeeee"}
    
    
    ## 第二次请求的返回结果:
    ## 限流起作用,返回403
    HTTP/1.1 403 Forbidden
    powered-by: proxy-wasm-go-sdk!!
    content-length: 29
    content-type: text/plain
    who-am-i: wasm-extension
    injected-by: istio-api!
    hally: wang
    wang: 1234567
    configdata: {"listconfig":["abc","def"],"testConfig":"abcddeeeee"}
    date: Wed, 31 May 2023 03:20:12 GMT
    server: envoy
    x-envoy-upstream-service-time: 0
    
    

从代码开始

安装tinygo

tinygo可以将go编译成wasm文件

https://tinygo.org/getting-started/install/

创建go工程

mkdir go-wasm-plugin
cd go-wasm-plugin
go mod init go-wasm

新增文件main.go

package main

import (
    "time"

    "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
    "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
)

func main() {
    proxywasm.SetVMContext(&vmContext{})
}

type vmContext struct {
    // Embed the default VM context here,
    // so that we don't need to reimplement all the methods.
    types.DefaultVMContext
}

// Override types.DefaultVMContext.
func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {
    return &pluginContext{}
}

type pluginContext struct {
    // Embed the default plugin context here,
    // so that we don't need to reimplement all the methods.
    types.DefaultPluginContext
    // the remaining token for rate limiting, refreshed periodically.
    remainToken int
    // // the preconfigured request per second for rate limiting.
    // requestPerSecond int
    // NOTE(jianfeih): any concerns about the threading and mutex usage for tinygo wasm?
    // the last time the token is refilled with `requestPerSecond`.
    lastRefillNanoSec int64
}

// Override types.DefaultPluginContext.
func (p *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
    return &httpHeaders{contextID: contextID, pluginContext: p}
}

type httpHeaders struct {
    // Embed the default http context here,
    // so that we don't need to reimplement all the methods.
    types.DefaultHttpContext
    contextID     uint32
    pluginContext *pluginContext
}

// Additional headers supposed to be injected to response headers.
var additionalHeaders = map[string]string{
    "who-am-i":    "go-wasm-extension",
    "injected-by": "istio-api!",
    "hally":       "wang",
    "wang":        "1234567",
    // 定义自定义的header,每个返回中都添加以上header
}

// 读取部署yaml中的 pluginConfig 内容,用于插件的一些配置信息
var configData string

func (p *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {
    proxywasm.LogDebug("loading plugin config")
    data, err := proxywasm.GetPluginConfiguration()
    if data == nil {
        return types.OnPluginStartStatusOK
    }

    if err != nil {
        proxywasm.LogCriticalf("error reading plugin configuration: %v", err)
        return types.OnPluginStartStatusFailed
    }

    // 插件启动的时候读取配置
    configData = string(data)

    return types.OnPluginStartStatusOK
}

func (ctx *httpHeaders) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
    //添加headr
    for key, value := range additionalHeaders {
        proxywasm.AddHttpResponseHeader(key, value)
    }

    //为了便于演示观察,将配置信息也加到返回头里
    proxywasm.AddHttpResponseHeader("configData", configData)
    return types.ActionContinue
}

// 实现限流
func (ctx *httpHeaders) OnHttpRequestHeaders(int, bool) types.Action {
    current := time.Now().UnixNano()
    // We use nanoseconds() rather than time.Second() because the proxy-wasm has the known limitation.
    // TODO(incfly): change to time.Second() once https://github.com/proxy-wasm/proxy-wasm-cpp-host/issues/199
    // is resolved and released.
    if current > ctx.pluginContext.lastRefillNanoSec+1e9 {
        ctx.pluginContext.remainToken = 2
        ctx.pluginContext.lastRefillNanoSec = current
    }
    proxywasm.LogCriticalf("Current time %v, last refill time %v, the remain token %v",
        current, ctx.pluginContext.lastRefillNanoSec, ctx.pluginContext.remainToken)
    if ctx.pluginContext.remainToken == 0 {
        if err := proxywasm.SendHttpResponse(403, [][2]string{
            {"powered-by", "proxy-wasm-go-sdk!!"},
        }, []byte("rate limited, wait and retry."), -1); err != nil {
            proxywasm.LogErrorf("failed to send local response: %v", err)
            proxywasm.ResumeHttpRequest()
        }
        return types.ActionPause
    }
    ctx.pluginContext.remainToken -= 1
    return types.ActionContinue
}

新增文件 Dockerfile

# Dockerfile for building "compat" variant of Wasm Image Specification.
# https://github.com/solo-io/wasm/blob/master/spec/spec-compat.md

FROM scratch

COPY main.wasm ./plugin.wasm

编译go代码为wasm

go get

tinygo build -o main.wasm -scheduler=none -target=wasi main.go

编译成功后,工程目录中将出现 main.wasm 文件

build一个Docker镜像,推送到镜像仓库

docker build . -t registy.cn-hangzhou.aliyuncs.com/USER_NAME/gowasm:0.1
docker push registy.cn-hangzhou.aliyuncs.com/USER_NAME/gowasm:0.1

新增部署yaml

gowasm.yaml

apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: my-go-wasm-plugin
  namespace: default
spec:
  selector:
    matchLabels:
      app: httpbin
  url: oci://registry.cn-hangzhou.aliyuncs.com/USER_NAME/gowasm:0.1
  pluginConfig:
    testConfig: abcddeeeee
    listconfig:
     - abc
     - def

部署到k8s,执行测试脚本

kubectl apply -f gowasm.yaml

SLEEP_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name}

kubectl exec ${SLEEP_POD} -c sleep -- sh -c 'for i in $(seq 1 3); do curl --head -s httpbin:8000/headers; sleep 0.1; done'

## 观察返回内容

删除插件

kubectl delete wasmplugins my-go-wasm-plugin

遇到的问题

  • 修改代码重新发布部署后,如果镜像的tag没变化,可能出现不生效,这是因为wasmplugin有自己的缓存机制,tag版本发生变化,不会出现该问题

本文代码

https://github.com/hallywang/go-wasm-plugin-example.git

其他玩法

  • istio官方提供的使用webassemblyhub来打包发布,内容参考

https://istio.io/latest/zh/blog/2020/deploy-wasm-declarative/

但是发现webassemblyhub提供的工具不支持最新版本的istio。

参考资料

https://tetrate.io/blog/istio-wasm-extensions-and-ecosystem/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容