WASMエッジゲートウェイ実践:GoでプラグインベースAPIゲートウェイを構築する5つのコア設計
APIゲートウェイに機能を追加するだけでリリースと再起動が必要
深夜2時、オンラインゲートウェイに急なレート制限ポリシー変更の要件が発生——KongプラグインはLuaでしか書けず、APISIXプラグインはOpenRestyでしか動かず、EnvoyネイティブFilterはC++でバイナリ全体を再コンパイルする必要があることに気づく。プラグイン言語の制限、ホットアップデート不可、セキュリティ隔離の不十分さ — 機能変更のたびにフルリリースが必要で、ゲートウェイチームが全社のボトルネックになっている。
2026年、WASMエッジゲートウェイがこの状況を一変させる。GoホストプロセスがWASMプラグインをロードし、TinyGo/Rust/AssemblyScriptから選択可能、プラグインはサンドボックス内で実行され脱出不可能、ホットリロードでゼロダウンタイム、Envoy WASM Filterとのシームレスな統合。本ガイドでは5つのコア設計を、ゲートウェイアーキテクチャからGoホストランタイム、WASMプラグイン開発からEnvoy Filter統合、エッジデプロイからホットリロードまで、完全なコード付きで解説する。
WASMエッジゲートウェイコア概念
| 概念 | 説明 |
|---|---|
| WASMゲートウェイ | WebAssemblyプラグインシステムベースのAPIゲートウェイ、プラグインは多言語開発、サンドボックス隔離、ホットロード可能 |
| WASMプラグイン | .wasmバイナリにコンパイルされたゲートウェイ拡張モジュール、サンドボックス内で実行、認証・レート制限・変換等をサポート |
| Envoy WASM Filter | EnvoyプロキシのWASMフィルター、Proxy-WASM SDKで開発、Go/C++/Rustに組み込み可能 |
| Proxy-WASM | Envoyが定義するWASMプロキシインターフェース仕様、onHttpRequest/onHttpResponse等のライフサイクルフックを定義 |
| エッジルーティング | CDN/エッジノードで実行されるAPIルーティングルール、リクエストを最寄りのバックエンドサービスにディスパッチ |
| プラグインサンドボックス | WASMリニアメモリ分離モデル、プラグインはホストプロセスメモリやシステムリソースにアクセス不可 |
| ホットリロード | 実行時にWASMプラグインモジュールを置き換え、ゲートウェイプロセスの再起動不要、リクエストゼロロス |
WASMエッジゲートウェイアーキテクチャ
┌──────────────────────────────────────────────────────┐
│ Edge Gateway (Go) │
│ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │
│ │ Route │ │ Plugin │ │ Plugin Chain │ │
│ │ Matcher │ │ Loader │ │ ┌──┐┌──┐┌──┐ │ │
│ └────┬─────┘ └─────┬─────┘ │ │Au││RL││Co│ │ │
│ │ │ │ │th││im││RS│ │ │
│ ┌────▼──────────────▼────┐ │ └──┘└──┘└──┘ │ │
│ │ Wazero Runtime │ └────────┬─────────┘ │
│ │ ┌──────┐ ┌──────┐ │ │ │
│ │ │Auth │ │Rate │ │ ┌─────────▼──────────┐ │
│ │ │WASM │ │Limit │ │ │ Reverse Proxy │ │
│ │ │Plugin│ │WASM │ │ │ (Upstream) │ │
│ │ └──────┘ └──────┘ │ └────────────────────┘ │
│ └───────────────────────┘ │
└──────────────────────────────────────────────────────┘
問題分析:従来のAPIゲートウェイの5つの限界
- プラグイン言語の制限:KongはLuaのみ、APISIXはLua/Goのみ(コンパイル必要)、EnvoyネイティブFilterはC++の再コンパイルが必要 — チームの技術スタックがゲートウェイに束縛される
- ホットアップデート不可:プラグインロジックの変更にはゲートウェイプロセスの再起動や設定の再読み込みが必要 — リスクが高くメンテナンスウィンドウが長い
- セキュリティ隔離の不十分さ:Luaプラグインがホストプロセスメモリに直接アクセス — 悪意のある/バグのあるプラグイン1つでゲートウェイインスタンス全体をクラッシュさせる可能性
- 拡張コストが高い:新しい認証方式やカスタムレート制限の追加にはゲートウェイソースコードの深い理解が必要 — 開発サイクルが長く、回帰テストの範囲が広い
- マルチゲートウェイの不統一:Kong/APISIX/Envoy/Nginxがそれぞれ独自のプラグイン体系を持ち — クロスゲートウェイポリシーの再利用が不可能、保守コストが倍増
設計1:WASMゲートウェイアーキテクチャ設計
WASMゲートウェイの核心的なアイデアはゲートウェイ機能をプラグイン可能なWASMモジュールに分解すること。Goホストがルーティングディスパッチとプラグインオーケストレーションを担当し、WASMプラグインが具体的なビジネスロジックを担当する。
package gateway
type GatewayConfig struct {
ListenAddr string `json:"listenAddr"`
Routes []RouteConfig `json:"routes"`
PluginChain []PluginConfig `json:"pluginChain"`
UpstreamPool map[string]string `json:"upstreamPool"`
}
type RouteConfig struct {
Path string `json:"path"`
Methods []string `json:"methods"`
PluginChain []string `json:"pluginChain"`
Upstream string `json:"upstream"`
Rewrite string `json:"rewrite,omitempty"`
}
type PluginConfig struct {
Name string `json:"name"`
WasmURL string `json:"wasmUrl"`
Config []byte `json:"config"`
}
type EdgeGateway struct {
config GatewayConfig
runtime *WASMRuntime
routes *RouteMatcher
plugins *PluginChainManager
proxy *ReverseProxy
}
func NewEdgeGateway(cfg GatewayConfig) (*EdgeGateway, error) {
rt, err := NewWASMRuntime()
if err != nil {
return nil, fmt.Errorf("init wasm runtime: %w", err)
}
pcm, err := NewPluginChainManager(rt, cfg.PluginChain)
if err != nil {
return nil, fmt.Errorf("init plugin chain: %w", err)
}
return &EdgeGateway{
config: cfg,
runtime: rt,
routes: NewRouteMatcher(cfg.Routes),
plugins: pcm,
proxy: NewReverseProxy(cfg.UpstreamPool),
}, nil
}
設計2:Goホストランタイム実装
wazeroランタイムを使用してWASMプラグインをロードし、各プラグインは独立したサンドボックスで実行され、エクスポート関数を通じてリクエスト/レスポンスインターセプトを実装する。
package gateway
import (
"context"
"net/http"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)
type WASMPlugin struct {
Name string
runtime wazero.Runtime
module api.Module
}
type WASMRuntime struct {
runtime wazero.Runtime
ctx context.Context
plugins map[string]*WASMPlugin
}
func NewWASMRuntime() (*WASMRuntime, error) {
ctx := context.Background()
rt := wazero.NewRuntime(ctx)
_, err := rt.NewHostModuleBuilder("env").
NewFunctionBuilder().
WithFunc(func(m api.Module, offset uint32, size uint32) {
buf := make([]byte, size)
m.Memory().Read(offset, buf)
}).
Export("log").
NewFunctionBuilder().
WithFunc(func(m api.Module, statusCode uint32, bodyOff uint32, bodyLen uint32) {
buf := make([]byte, bodyLen)
m.Memory().Read(bodyOff, buf)
}).
Export("sendLocalReply").
Instantiate(ctx)
if err != nil {
return nil, err
}
wasi_snapshot_preview1.MustInstantiate(ctx, rt)
return &WASMRuntime{
runtime: rt,
ctx: ctx,
plugins: make(map[string]*WASMPlugin),
}, nil
}
func (wr *WASMRuntime) LoadPlugin(name string, wasmBytes []byte) (*WASMPlugin, error) {
mod, err := wr.runtime.Instantiate(wr.ctx, wasmBytes)
if err != nil {
return nil, fmt.Errorf("instantiate plugin %s: %w", name, err)
}
plugin := &WASMPlugin{
Name: name,
runtime: wr.runtime,
module: mod,
}
wr.plugins[name] = plugin
return plugin, nil
}
func (p *WASMPlugin) ExecuteRequestFilter(req *http.Request) (*http.Response, bool) {
ctx := context.Background()
fn := p.module.ExportedFunction("onHttpRequest")
if fn == nil {
return nil, true
}
results, err := fn.Call(ctx)
if err != nil {
return nil, true
}
return nil, results[0] == 0
}
func (p *WASMPlugin) ExecuteResponseFilter(resp *http.Response) bool {
ctx := context.Background()
fn := p.module.ExportedFunction("onHttpResponse")
if fn == nil {
return true
}
results, err := fn.Call(ctx)
if err != nil {
return true
}
return results[0] == 0
}
設計3:WASMプラグイン開発
TinyGoを使用して認証とレート制限プラグインを作成し、WASIターゲットにコンパイル、onHttpRequestとonHttpResponseのライフサイクルフックをエクスポートする。
package main
import "unsafe"
var memoryBuffer []byte
//export allocate
func allocate(size int32) unsafe.Pointer {
memoryBuffer = make([]byte, size)
return unsafe.Pointer(&memoryBuffer[0])
}
//export onHttpRequest
func onHttpRequest() int32 {
headers := getHeaders()
token := getHeader(headers, "Authorization")
if token == "" {
sendLocalReply(401, "Unauthorized")
return 1
}
if !validateToken(token) {
sendLocalReply(403, "Forbidden")
return 1
}
return 0
}
//export onHttpResponse
func onHttpResponse() int32 {
addHeader("X-Response-Time", getCurrentTime())
addHeader("X-Gateway-Version", "1.0.0")
return 0
}
func validateToken(token string) bool {
if len(token) < 10 {
return false
}
return true
}
func getHeaders() map[string]string {
return make(map[string]string)
}
func getHeader(headers map[string]string, key string) string {
return headers[key]
}
func sendLocalReply(statusCode int32, body string) {}
func addHeader(key string, value string) {}
func getCurrentTime() string {
return "2026-06-18T00:00:00Z"
}
func main() {}
レート制限プラグインの例:
package main
import "unsafe"
var requestCount int32 = 0
var maxRequests int32 = 1000
//export onHttpRequest
func onHttpRequest() int32 {
requestCount++
if requestCount > maxRequests {
sendLocalReply(429, "Too Many Requests")
return 1
}
addHeader("X-RateLimit-Remaining", int32ToStr(maxRequests-requestCount))
return 0
}
func int32ToStr(n int32) string {
if n == 0 {
return "0"
}
var buf [12]byte
pos := len(buf)
for n > 0 {
pos--
buf[pos] = byte('0' + n%10)
n /= 10
}
return string(buf[pos:])
}
func sendLocalReply(code int32, body string) {}
func addHeader(key, val string) {}
func main() {}
ビルドコマンド:
tinygo build -o auth-plugin.wasm -target=wasi -no-debug -scheduler=none .
tinygo build -o ratelimit-plugin.wasm -target=wasi -no-debug -scheduler=none .
設計4:Envoy WASM Filter統合
Proxy-WASM SDKを通じてEnvoy互換のWASM Filterを開発し、Envoy/Istioサービスメッシュとのシームレスな統合を実現する。
package main
import (
"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 {
types.DefaultVMContext
}
func (v *vmContext) NewPluginContext(contextID uint32) types.PluginContext {
return &pluginContext{}
}
type pluginContext struct {
types.DefaultPluginContext
maxRequests int32
}
func (p *pluginContext) OnPluginStart(configurationSize int) types.OnPluginStartStatus {
data, err := proxywasm.GetPluginConfiguration(configurationSize)
if err != nil && err != types.ErrorStatusNotFound {
proxywasm.LogCriticalf("error reading config: %v", err)
return types.OnPluginStartStatusFailed
}
if len(data) > 0 {
p.maxRequests = parseInt32(data)
}
return types.OnPluginStartStatusOK
}
func (p *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
return &httpContext{maxRequests: p.maxRequests}
}
type httpContext struct {
types.DefaultHttpContext
maxRequests int32
requestCount int32
}
func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
ctx.requestCount++
if ctx.requestCount > ctx.maxRequests {
proxywasm.SendLocalReply(429, "rate limited", nil, 0, "")
return types.ActionPause
}
token, err := proxywasm.GetHttpRequestHeader("authorization")
if err != nil || token == "" {
proxywasm.SendLocalReply(401, "unauthorized", nil, 0, "")
return types.ActionPause
}
proxywasm.AddHttpRequestHeader("x-wasm-filter", "envoy-auth-v1")
return types.ActionContinue
}
func (ctx *httpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
proxywasm.AddHttpResponseHeader("x-wasm-processed", "true")
return types.ActionContinue
}
func parseInt32(data []byte) int32 {
var result int32
for _, b := range data {
if b >= '0' && b <= '9' {
result = result*10 + int32(b-'0')
}
}
return result
}
Envoy設定統合:
name: envoy.filters.http.wasm
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
config:
name: auth-filter
root_id: auth-filter
vm_config:
vm_id: auth-filter-vm
runtime: envoy.wasm.runtime.v8
code:
local:
filename: /etc/envoy/auth-filter.wasm
configuration:
"@type": type.googleapis.com/google.protobuf.StringValue
value: "1000"
設計5:エッジデプロイとホットリロード
プラグインのリモートロードとバージョン管理を実装し、S3/OSSからのWASMモジュール取得をサポート、ゼロダウンタイムでのバージョンロールバックを実現する。
package gateway
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"sync"
"sync/atomic"
"time"
)
type PluginVersion struct {
Name string `json:"name"`
Version string `json:"version"`
SHA256 string `json:"sha256"`
WasmURL string `json:"wasmUrl"`
LoadedAt int64 `json:"loadedAt"`
}
type PluginRegistry struct {
runtime *WASMRuntime
versions map[string]*PluginVersion
current sync.Map
mu sync.RWMutex
httpClient *http.Client
}
func NewPluginRegistry(rt *WASMRuntime) *PluginRegistry {
return &PluginRegistry{
runtime: rt,
versions: make(map[string]*PluginVersion),
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
func (pr *PluginRegistry) LoadFromRemote(name string, version PluginVersion) error {
resp, err := pr.httpClient.Get(version.WasmURL)
if err != nil {
return fmt.Errorf("fetch wasm from %s: %w", version.WasmURL, err)
}
defer resp.Body.Close()
var buf bytes.Buffer
if _, err := io.Copy(&buf, resp.Body); err != nil {
return fmt.Errorf("read wasm body: %w", err)
}
wasmBytes := buf.Bytes()
hash := sha256.Sum256(wasmBytes)
checksum := hex.EncodeToString(hash[:])
if checksum != version.SHA256 {
return fmt.Errorf("checksum mismatch: expected %s, got %s", version.SHA256, checksum)
}
plugin, err := pr.runtime.LoadPlugin(name, wasmBytes)
if err != nil {
return fmt.Errorf("load plugin %s: %w", name, err)
}
pr.mu.Lock()
old, exists := pr.versions[name]
pr.versions[name] = &version
pr.mu.Unlock()
pr.current.Store(name, plugin)
if exists {
fmt.Printf("plugin %s upgraded: %s -> %s\n", name, old.Version, version.Version)
} else {
fmt.Printf("plugin %s loaded: v%s\n", name, version.Version)
}
return nil
}
type PluginChainManager struct {
registry *PluginRegistry
chains map[string][]string
mu sync.RWMutex
}
func NewPluginChainManager(rt *WASMRuntime, configs []PluginConfig) (*PluginChainManager, error) {
registry := NewPluginRegistry(rt)
pcm := &PluginChainManager{
registry: registry,
chains: make(map[string][]string),
}
for _, cfg := range configs {
version := PluginVersion{
Name: cfg.Name,
Version: "1.0.0",
WasmURL: cfg.WasmURL,
}
if err := registry.LoadFromRemote(cfg.Name, version); err != nil {
return nil, fmt.Errorf("load plugin %s: %w", cfg.Name, err)
}
}
return pcm, nil
}
func (pcm *PluginChainManager) ExecuteChain(chainName string, req *http.Request) (*http.Response, bool) {
pcm.mu.RLock()
chain, exists := pcm.chains[chainName]
pcm.mu.RUnlock()
if !exists {
return nil, true
}
for _, pluginName := range chain {
plugin, ok := pcm.registry.current.Load(pluginName)
if !ok {
continue
}
wasmPlugin := plugin.(*WASMPlugin)
resp, allowed := wasmPlugin.ExecuteRequestFilter(req)
if !allowed {
return resp, false
}
}
return nil, true
}
ゲートウェイルーティング + プラグインチェーン完全例:
package main
import (
"log"
"net/http"
"github.com/yourorg/edge-gateway/gateway"
)
func main() {
cfg := gateway.GatewayConfig{
ListenAddr: ":8080",
Routes: []gateway.RouteConfig{
{
Path: "/api/v1/users",
Methods: []string{"GET", "POST"},
PluginChain: []string{"auth-plugin", "ratelimit-plugin"},
Upstream: "user-service",
},
{
Path: "/api/v1/orders",
Methods: []string{"GET", "POST", "PUT"},
PluginChain: []string{"auth-plugin", "ratelimit-plugin", "cors-plugin"},
Upstream: "order-service",
},
},
PluginChain: []gateway.PluginConfig{
{Name: "auth-plugin", WasmURL: "https://cdn.example.com/plugins/auth-v1.2.0.wasm"},
{Name: "ratelimit-plugin", WasmURL: "https://cdn.example.com/plugins/ratelimit-v1.1.0.wasm"},
{Name: "cors-plugin", WasmURL: "https://cdn.example.com/plugins/cors-v1.0.0.wasm"},
},
UpstreamPool: map[string]string{
"user-service": "http://127.0.0.1:8081",
"order-service": "http://127.0.0.1:8082",
},
}
gw, err := gateway.NewEdgeGateway(cfg)
if err != nil {
log.Fatalf("create gateway: %v", err)
}
mux := http.NewServeMux()
mux.HandleFunc("/", gw.HandleRequest)
log.Printf("WASM Edge Gateway listening on %s", cfg.ListenAddr)
log.Fatal(http.ListenAndServe(cfg.ListenAddr, mux))
}
よくある落とし穴:5つの共通トラップ
落とし穴1:WASMプラグインがHTTP接続を直接操作
// ❌ 間違い:WASMサンドボックス内でTCP接続を確立できない
conn, _ := net.Dial("tcp", "backend:8080")
// ✅ 正しい:ホスト提供のHost Functionを通じてネットワークリクエストをプロキシ
results, _ := hostNetDial.Call(ctx, uint64(offset), uint64(len(host)), uint64(port))
落とし穴2:プラグイン間の状態共有によるデータ競合
// ❌ 間違い:複数のWASMプラグインインスタンスがグローバル変数を共有
var tokenCache = make(map[string]bool)
// ✅ 正しい:各プラグインインスタンスは独立したメモリを使用、状態はホスト経由で共有
// ホスト側でKV Store Host Functionを提供
落とし穴3:WASMプラグイン実行タイムアウトの無視
// ❌ 間違い:プラグインの戻りを無限に待機、悪意のある/無限ループプラグインがリクエスト全体をブロック
results, err := fn.Call(ctx)
// ✅ 正しい:context.WithTimeoutを使用して実行時間を制御
timeoutCtx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
defer cancel()
results, err := fn.Call(timeoutCtx)
落とし穴4:ホットリロード時の実行中リクエストの未処理
// ❌ 間違い:古いモジュールを直接アンロード、実行中のリクエストが解放済みメモリにアクセス
oldModule.Close(ctx)
// ✅ 正しい:参照カウント + グレースフルウェイト、全リクエスト完了後にアンロード
atomic.AddInt32(&refCount, 1)
defer atomic.AddInt32(&refCount, -1)
// ホットリロード時、refCountが0になるまで待機してからアンロード
落とし穴5:TinyGoビルドでWASIターゲットを未指定
# ❌ 間違い:-wasmターゲットを使用するとWASIインポートが欠落
tinygo build -o plugin.wasm -target=wasm .
# ✅ 正しい:wasiターゲットを使用して標準システムコールをサポート
tinygo build -o plugin.wasm -target=wasi -no-debug -scheduler=none .
エラートラブルシューティング:10の共通エラー
| # | エラーメッセージ | 原因 | 解決方法 |
|---|---|---|---|
| 1 | out of bounds memory access |
ホストがWASMメモリオフセットの境界外に読み書き | allocate戻り値とデータ長計算を確認 |
| 2 | function export not found: onHttpRequest |
WASMモジュールがライフサイクルフックをエクスポートしていない | TinyGoコンパイル時に//export onHttpRequestをエクスポート |
| 3 | import not found: wasi_snapshot_preview1 |
WASIモジュールが初期化されていない | wasi_snapshot_preview1.MustInstantiateを呼び出し |
| 4 | instantiation error: memory size exceeds limit |
プラグインの初期メモリが制限を超過 | WithMemoryLimitPagesを増やすかメモリ使用量を最適化 |
| 5 | context deadline exceeded in plugin execution |
プラグイン実行がタイムアウト | context.WithTimeoutを使用しプラグインロジックを確認 |
| 6 | checksum mismatch |
リモートロードしたWASMファイルの検証失敗 | ファイルの完全性を確認、WASMモジュールを再アップロード |
| 7 | plugin version conflict |
同名プラグインのバージョン競合 | セマンティックバージョニングを使用し一意性を確保 |
| 8 | goroutine stack overflow in wasm |
TinyGoデフォルトスケジューラによるスタックオーバーフロー | コンパイル時に-scheduler=noneを追加 |
| 9 | wasm validation error: invalid section |
WASMバイナリファイルが破損 | WASMモジュールを再コンパイル、ビルドコマンドを確認 |
| 10 | hot reload: old module still referenced |
ホットリロード時に古いモジュールの参照が未解放 | リクエスト参照カウントがゼロになるまでアンロードを待機 |
高度な最適化テクニック
1. WASMプラグイン事前コンパイルキャッシュ
type CompiledCache struct {
cache map[string]wazero.CompiledModule
mu sync.RWMutex
}
func (cc *CompiledCache) GetOrCompile(ctx context.Context, rt wazero.Runtime, key string, wasmBytes []byte) (wazero.CompiledModule, error) {
cc.mu.RLock()
if compiled, ok := cc.cache[key]; ok {
cc.mu.RUnlock()
return compiled, nil
}
cc.mu.RUnlock()
compiled, err := rt.CompileModule(ctx, wasmBytes)
if err != nil {
return nil, err
}
cc.mu.Lock()
cc.cache[key] = compiled
cc.mu.Unlock()
return compiled, nil
}
2. プラグインチェーン並列実行
func (pcm *PluginChainManager) ExecuteChainParallel(chainName string, req *http.Request) (*http.Response, bool) {
pcm.mu.RLock()
chain := pcm.chains[chainName]
pcm.mu.RUnlock()
type result struct {
resp *http.Response
allowed bool
}
ch := make(chan result, len(chain))
for _, pluginName := range chain {
go func(name string) {
plugin, _ := pcm.registry.current.Load(name)
if plugin == nil {
ch <- result{nil, true}
return
}
resp, allowed := plugin.(*WASMPlugin).ExecuteRequestFilter(req)
ch <- result{resp, allowed}
}(pluginName)
}
for i := 0; i < len(chain); i++ {
r := <-ch
if !r.allowed {
return r.resp, false
}
}
return nil, true
}
3. プラグインメトリクス収集
type PluginMetrics struct {
Name string
ExecCount atomic.Int64
ErrorCount atomic.Int64
TotalTimeNs atomic.Int64
}
func (m *PluginMetrics) Record(execTime time.Duration, hasError bool) {
m.ExecCount.Add(1)
m.TotalTimeNs.Add(execTime.Nanoseconds())
if hasError {
m.ErrorCount.Add(1)
}
}
func (m *PluginMetrics) AvgTime() time.Duration {
count := m.ExecCount.Load()
if count == 0 {
return 0
}
return time.Duration(m.TotalTimeNs.Load() / count)
}
4. プラグイン設定ホットインジェクション
func (wr *WASMRuntime) InjectConfig(pluginName string, configJSON []byte) error {
plugin, ok := wr.plugins[pluginName]
if !ok {
return fmt.Errorf("plugin %s not found", pluginName)
}
fn := plugin.module.ExportedFunction("onConfigUpdate")
if fn == nil {
return nil
}
offset, err := writeString(plugin.module, string(configJSON))
if err != nil {
return err
}
_, err = fn.Call(wr.ctx, uint64(offset), uint64(len(configJSON)))
return err
}
5. エッジノードプラグイン配信
type EdgeDistributor struct {
registry *PluginRegistry
nodes []string
httpClient *http.Client
}
func (ed *EdgeDistributor) PushToEdge(pluginName string, version PluginVersion) error {
for _, node := range ed.nodes {
url := fmt.Sprintf("http://%s/api/v1/plugins/load", node)
payload, _ := json.Marshal(map[string]interface{}{
"name": pluginName,
"version": version.Version,
"wasmUrl": version.WasmURL,
"sha256": version.SHA256,
})
resp, err := ed.httpClient.Post(url, "application/json", bytes.NewReader(payload))
if err != nil {
fmt.Printf("push to %s failed: %v\n", node, err)
continue
}
resp.Body.Close()
fmt.Printf("pushed %s v%s to %s\n", pluginName, version.Version, node)
}
return nil
}
比較分析:WASMゲートウェイ vs Kong vs APISIX vs Envoyネイティブ
| 次元 | WASMゲートウェイ(Go) | Kong | APISIX | Envoyネイティブ |
|---|---|---|---|---|
| プラグイン言語 | Go/TinyGo/Rust/AS | Lua | Lua/Go(外部) | C++ |
| ホットリロード | WASMモジュールホットロード | 再起動/DB同期が必要 | etcd watch | バイナリ再コンパイルが必要 |
| セキュリティ隔離 | WASMサンドボックス強隔離 | Lua共有プロセスメモリ | Lua共有プロセスメモリ | プロセスレベル隔離 |
| プラグインサイズ | 100KB-2MB | Luaスクリプト<50KB | Luaスクリプト<50KB | バイナリ全体の再コンパイルが必要 |
| 実行パフォーマンス | ネイティブに近い(90-95%) | LuaJIT高性能 | LuaJIT高性能 | ネイティブC++最速 |
| マルチゲートウェイ再利用 | WASMモジュール汎用 | Kongのみ | APISIXのみ | Envoyのみ |
| 学習曲線 | WASM+Goの知識が必要 | Luaはシンプル | Luaはシンプル | C++のハードルが高い |
| エコシステム成熟度 | 急成長(2026) | 成熟(500+プラグイン) | 成熟(100+プラグイン) | 成熟(Istioエコシステム) |
オンラインツール推薦
WASMエッジゲートウェイの開発時に、以下のオンラインツールが生産性を大幅に向上させる:
- JSONフォーマッター — ゲートウェイプラグイン設定やルーティングルールJSONのデバッグ時に素早くフォーマットと検証
- ハッシュ計算ツール — WASMモジュールのSHA256チェックサムを計算し、リモートロード時のプラグイン整合性を検証
- Curl→コード変換ツール — curlリクエストをGo HTTPコードに変換し、ゲートウェイテストケースを素早く作成
まとめと展望
WASMエッジゲートウェイはKongやEnvoyを置き換えるものではなく、それらの上に言語非依存、セキュア隔離、ホットリロード可能なプラグイン抽象化レイヤーを提供する。2026年のトレンドは:Envoyがデータプレーンプロキシ、WASM Filterがポータブルなプラグインレイヤー、Goホストが軽量ゲートウェイオーケストレーター。この3つの組み合わせで、Envoyの本番レベルの安定性 + WASMのプラグイン柔軟性 + Goの開発効率が得られる。次のステップとして、WASM Component ModelとWASI Preview 3の標準化進展に注目 — これらがクロス言語プラグインの相互運用性問題を完全に解決する。
さらなる学習
- Proxy-WASM ABI Specification — Envoy WASM Filterインターフェース仕様
- Wazero Runtime Documentation — 純粋なGo WASMランタイム公式ドキュメント
- Envoy WASM Filter Design — Envoy WASMフィルター設計ドキュメント
- WebAssembly Component Model — WASMコンポーネントモデル提案
- TinyGo WASI Target Guide — TinyGo WASIコンパイルガイド
ブラウザローカルツールを無料で試す →