カスタム Go プラグインをビルドして AI ゲートウェイの機能を拡張します。このドキュメントでは、プラグインの構造、HTTP 処理フック、Docker Compose を使用したローカルデバッグについて説明します。
Higress は、TinyGo 0.29 + Go 1.20 から、Wasm コンパイルをネイティブにサポートする Go 1.24 に移行しました。
TinyGo から Go 1.24 に移行する場合、go.mod の依存関係を更新し、プラグインの初期化を main から init に移動する必要があります。
TinyGo ベースのプラグインを対応させる場合:
1. ヘッダー処理中に、type.ActionPause を types.HeaderStopAllIterationAndWatermark に置き換えてください。例については、「外部 HTTP サービスの呼び出し」セクションをご参照ください。
2. go-re2 ライブラリを標準の Go regexp パッケージに置き換えてください。
前提条件
Go 1.24 以降をインストールしてください。
Go
公式 ガイド に従って Go 1.24 以降をインストールしてください。
Go 1.24 でコンパイルされたプラグインには、AI ゲートウェイのバージョン 2.1.5 以降が必要です。「Go を使用した WASM プラグインの開発」では、以前のゲートウェイバージョンについて説明しています。
Windows
インストールファイルをダウンロードしてください。
インストーラーを実行してください。デフォルトでは、Go は
Program FilesまたはProgram Files (x86)にインストールされます。Win+R を押し、
cmdと入力して OK をクリックします。go versionを実行してインストールを確認してください。
macOS
インストールファイルをダウンロードしてください。
インストーラーを実行してください。デフォルトでは、Go は
/usr/local/goにインストールされます。ターミナルで
go versionを実行してインストールを確認してください。
Linux
インストールファイルをダウンロードしてください。
これらのコマンドを実行して Go をインストールしてください。
Go をインストールします。
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.24.4.linux-amd64.tar.gz環境変数を設定します。
export PATH=$PATH:/usr/local/go/bingo versionを実行してインストールを確認してください。
プラグインの作成
プロジェクトの初期化
プロジェクトディレクトリを作成し、Go モジュールを初期化します:
mkdir wasm-demo-go && cd wasm-demo-go go mod init wasm-demo-goプラグイン SDK の依存関係をダウンロードします:
go get github.com/higress-group/proxy-wasm-go-sdk@go-1.24 go get github.com/higress-group/wasm-go@main go get github.com/tidwall/gjson中国本土にいる場合は、まずプロキシを設定してください:
go env -w GOPROXY=https://proxy.golang.com.cn,direct
main.go ファイルの作成
次の例では、すべての受信リクエストに hello: world リクエストヘッダーを追加します。プラグイン設定で mockEnable が true の場合、プラグインはバックエンドに転送する代わりに、直接 hello world を返します。
package main
import (
"github.com/higress-group/wasm-go/pkg/wrapper"
logs "github.com/higress-group/wasm-go/pkg/log"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
func main() {}
func init() {
wrapper.SetCtx(
// プラグイン名
"my-plugin",
// カスタム設定パーサー
wrapper.ParseConfigBy(parseConfig),
// リクエストヘッダー処理フェーズへのフック
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
// カスタムプラグイン設定
type MyConfig struct {
mockEnable bool
}
// JSON 設定を config 構造体にパースします。
// ゲートウェイコンソールは YAML を JSON に自動的に変換します。
func parseConfig(json gjson.Result, config *MyConfig, log logs.Log) error {
config.mockEnable = json.Get("mockEnable").Bool()
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log logs.Log) types.Action {
proxywasm.AddHttpRequestHeader("hello", "world")
if config.mockEnable {
proxywasm.SendHttpResponse(200, nil, []byte("hello world"), -1)
}
return types.HeaderContinue
}重要なポイント:
main()関数は空でなければなりません。すべての初期化ロジックはinit()に配置してください。wrapper.SetCtxは、プラグイン名、設定パーサー、および処理フックを登録します。リクエストを次のフィルターに渡すには、
types.HeaderContinue(types.ActionContinueと同等) を返します。
SDK リファレンス
ユーティリティメソッド
プラグイン SDK は、リクエストとレスポンスを操作するための次の proxywasm メソッドを提供します:
リクエストヘッダーの処理 (リクエストヘッダーフェーズで有効)
メソッド | 目的 |
| すべてのリクエストヘッダーを取得 |
| すべてのリクエストヘッダーを置換 |
| 特定のリクエストヘッダーを取得 |
| 特定のリクエストヘッダーを削除 |
| 特定のリクエストヘッダーを置換 |
| リクエストヘッダーを追加 |
リクエストボディの処理 (リクエストボディフェーズで有効)
メソッド | 目的 |
| リクエストボディを取得 |
| リクエストボディの末尾にデータを追加 |
| リクエストボディの先頭にデータを追加 |
| リクエストボディ全体を置換 |
レスポンスヘッダーの処理 (レスポンスヘッダーフェーズで有効)
メソッド | 目的 |
| バックエンドからすべてのレスポンスヘッダーを取得 |
| すべてのレスポンスヘッダーを置換 |
| 特定のレスポンスヘッダーを取得 |
| 特定のレスポンスヘッダーを削除 |
| 特定のレスポンスヘッダーを置換 |
| レスポンスヘッダーを追加 |
レスポンスボディの処理 (レスポンスボディフェーズで有効)
メソッド | 目的 |
| レスポンスボディを取得 |
| レスポンスボディの末尾にデータを追加 |
| レスポンスボディの先頭にデータを追加 |
| レスポンスボディ全体を置換 |
HTTP 呼び出しとフロー制御
メソッド | 目的 |
| 外部サービスへ HTTP リクエストを送信 |
|
|
|
|
|
|
| クライアントに直接 HTTP レスポンスを返す |
| 一時停止したリクエスト処理フローを再開 |
| 一時停止したレスポンス処理フローを再開 |
処理が一時停止していない場合は、ResumeHttpRequest または ResumeHttpResponse を呼び出さないでください。SendHttpResponse が呼び出されると、一時停止状態が解消され、クライアントに直接レスポンスが送信されます。そのため、再度 ResumeHttpRequest または ResumeHttpResponse を呼び出すと、未定義の動作が発生します。
ヘッダーのステータスコード
各処理フックは、リクエストフローを制御するヘッダーのステータスコードを返します。プラグインがデータを一時停止、バッファリング、またはストリーミングする必要があるかどうかに基づいて、適切なステータスを選択してください:
ステータス | 動作 |
| 現在のフィルター処理を完了し、リクエストを次のフィルターに渡します。 |
| ヘッダーを保持しますが、ボディデータの読み取りは続行します。ボディ処理フェーズ中にリクエストヘッダーを変更する場合に使用します。ボディが必要です -- ボディが存在しない場合、リクエストは無期限にブロックされます。 |
| ヘッダーを |
| すべての反復を停止し、ヘッダー、ボディ、およびトレーラーをバッファリングします。バッファが制限を超えた場合、ゲートウェイはリクエストフェーズ中に 413、レスポンスフェーズ中に 500 を返します。 |
|
|
HeaderStopIteration と HeaderStopAllIterationAndWatermark の実際の例については、Higress の ai-transformer プラグインと ai-quota プラグインをご参照ください。
Wasm ファイルのコンパイル
プラグインを Wasm バイナリにコンパイルします:
go mod tidy
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./これにより、main.wasm ファイルが生成されます。このファイルを使用してローカルデバッグを行うか、クラウドネイティブゲートウェイのマーケットプレイスを通じてアップロードしてカスタムプラグインをデプロイできます。
Higress を介して WasmPlugin カスタムリソース定義 (CRD) またはコンソール UI を使用してデプロイするには、Wasm ファイルを OCI または Docker イメージにパッケージ化する必要があります。詳細については、「カスタムプラグイン」をご参照ください。
ローカルデバッグ
前提条件
Docker をインストールしてください。
テスト環境のセットアップ
プロジェクトディレクトリに main.wasm が存在することを確認してから、次の 2 つのファイルを作成してください。
docker-compose.yaml
version: '3.7'
services:
envoy:
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v2.1.5
entrypoint: /usr/local/bin/envoy
# Wasm のデバッグレベルのロギングを有効にします。本番環境では info レベルを使用してください。
command: -c /etc/envoy/envoy.yaml --component-log-level wasm:debug
depends_on:
- httpbin
networks:
- wasmtest
ports:
- "10000:10000"
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml
- ./main.wasm:/etc/envoy/main.wasm
httpbin:
image: kennethreitz/httpbin:latest
networks:
- wasmtest
ports:
- "12345:80"
networks:
wasmtest: {}envoy.yaml
admin:
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 9901
static_resources:
listeners:
- name: listener_0
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
scheme_header_transformation:
scheme_to_overwrite: https
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: httpbin
http_filters:
- name: wasmdemo
typed_config:
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
value:
config:
name: wasmdemo
vm_config:
runtime: envoy.wasm.runtime.v8
code:
local:
filename: /etc/envoy/main.wasm
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{
"mockEnable": false
}
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: httpbin
connect_timeout: 30s
type: LOGICAL_DNS
# v6 ネットワークでテストする場合は、次の行をコメントアウトします
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: httpbin
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: httpbin
port_value: 80環境を起動します:
docker compose up検証
追加されたヘッダーをテストします。 ゲートウェイ (ポート 10000) を介してリクエストを送信し、Hello: world ヘッダーが表示されることを確認してください:
curl http://127.0.0.1:10000/get期待されるレスポンス:
{
"args": {},
"headers": {
"Accept": "*/*",
"Hello": "world",
"Host": "127.0.0.1:10000",
"Original-Host": "127.0.0.1:10000",
"Req-Start-Time": "1681269273896",
"User-Agent": "curl/7.79.1",
"X-Envoy-Expected-Rq-Timeout-Ms": "15000"
},
"origin": "172.18.0.3",
"url": "https://127.0.0.1:10000/get"
}Hello: world ヘッダーは、プラグインがアクティブであることを示します。
比較のために、httpbin (ポート 12345) への直接リクエストにはこのヘッダーは含まれません:
curl http://127.0.0.1:12345/get設定の変更をテストします。 envoy.yaml を編集し、mockEnable を true に設定してください:
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{
"mockEnable": true
}環境を再起動し、同じリクエストを送信してください:
curl http://127.0.0.1:10000/get期待されるレスポンス:
hello worldモックのレスポンスは、設定変更が正しく反映されていることを示します。
その他の例
設定なしのプラグイン
設定を必要としないプラグインの場合は、空の config 構造体を定義し、ParseConfigBy を省略してください:
package main
import (
"github.com/higress-group/wasm-go/pkg/wrapper"
logs "github.com/higress-group/wasm-go/pkg/log"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
)
func main() {}
func init() {
wrapper.SetCtx(
"hello-world",
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
type MyConfig struct {}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log logs.Log) types.Action {
proxywasm.SendHttpResponse(200, nil, []byte("hello world"), -1)
return types.HeaderContinue
}外部 HTTP サービスの呼び出し
プラグインは、Nacos サービス、Kubernetes サービス、およびゲートウェイコンソールで設定された固定アドレスまたは DNS サービスへの HTTP 呼び出しをサポートします。標準の net/http ライブラリは Wasm ランタイムでは使用できません。代わりに、SDK のカプセル化された HTTP クライアントを使用してください。
次の例では、起動時にサービス設定をパースし、リクエスト処理中にサービスを呼び出します。レスポンスヘッダーからトークンを抽出し、元のリクエストに挿入します。
package main
import (
"errors"
"net/http"
"strings"
"github.com/higress-group/wasm-go/pkg/wrapper"
logs "github.com/higress-group/wasm-go/pkg/log"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
func main() {}
func init() {
wrapper.SetCtx(
"http-call",
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
type MyConfig struct {
// 外部呼び出し用の HTTP クライアント
client wrapper.HttpClient
// 外部サービスへのリクエストパス
requestPath string
// 抽出して元のリクエストに挿入するレスポンスヘッダーキー
tokenHeader string
}
func parseConfig(json gjson.Result, config *MyConfig, log logs.Log) error {
config.tokenHeader = json.Get("tokenHeader").String()
if config.tokenHeader == "" {
return errors.New("missing tokenHeader in config")
}
config.requestPath = json.Get("requestPath").String()
if config.requestPath == "" {
return errors.New("missing requestPath in config")
}
// サービスタイプのサフィックスが付いた完全修飾ドメイン名 (FQDN)。
// 例:my-svc.dns, my-svc.static,
// service-provider.DEFAULT-GROUP.public.nacos,
// httpbin.my-ns.svc.cluster.local
serviceName := json.Get("serviceName").String()
servicePort := json.Get("servicePort").Int()
if servicePort == 0 {
if strings.HasSuffix(serviceName, ".static") {
// 静的 IP サービスのデフォルトポート
servicePort = 80
}
}
config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: serviceName,
Port: servicePort,
})
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log logs.Log) types.Action {
// 非同期 HTTP GET リクエストを送信します。デフォルトのタイムアウトは 500 ms です。
err := config.client.Get(config.requestPath, nil,
// レスポンスが到着したときに実行されるコールバック
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
if statusCode != http.StatusOK {
log.Errorf("http call failed, status: %d", statusCode)
proxywasm.SendHttpResponse(http.StatusInternalServerError, nil,
[]byte("http call failed"), -1)
return
}
log.Infof("get status: %d, response body: %s", statusCode, responseBody)
// レスポンスからトークンを抽出し、元のリクエストに追加します
token := responseHeaders.Get(config.tokenHeader)
if token != "" {
proxywasm.AddHttpRequestHeader(config.tokenHeader, token)
}
// 一時停止したリクエストを再開し、バックエンドに転送できるようにします
proxywasm.ResumeHttpRequest()
})
if err != nil {
// サービス呼び出しが失敗した場合は、リクエストの処理を続行し、エラーをログに記録します
log.Errorf("Error occured while calling http, it seems cannot find the service cluster.")
return types.ActionContinue
} else {
// 非同期コールバックが完了するまでリクエストを一時停止します
return types.HeaderStopAllIterationAndWatermark
}
}プラグインから Redis を呼び出す
次の例では、Redis をバックエンドとするレート制限プラグインを実装します。1 分あたりのリクエスト数 (QPM) を追跡し、制限を超えると HTTP 429 を返します。
package main
import (
"strconv"
"strings"
"time"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
"github.com/tidwall/resp"
"github.com/higress-group/wasm-go/pkg/wrapper"
logs "github.com/higress-group/wasm-go/pkg/log"
)
func main() {}
func init() {
wrapper.SetCtx(
"redis-demo",
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
)
}
type RedisCallConfig struct {
client wrapper.RedisClient
qpm int
}
func parseConfig(json gjson.Result, config *RedisCallConfig, log logs.Log) error {
// サービスタイプのサフィックスが付いた完全修飾ドメイン名 (FQDN)。
// 例:my-redis.dns, redis.my-ns.svc.cluster.local
serviceName := json.Get("serviceName").String()
servicePort := json.Get("servicePort").Int()
if servicePort == 0 {
if strings.HasSuffix(serviceName, ".static") {
servicePort = 80
} else {
servicePort = 6379
}
}
username := json.Get("username").String()
password := json.Get("password").String()
// タイムアウト (ミリ秒)
timeout := json.Get("timeout").Int()
if timeout == 0 {
timeout = 1000
}
qpm := json.Get("qpm").Int()
config.qpm = int(qpm)
config.client = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
FQDN: serviceName,
Port: servicePort,
})
return config.client.Init(username, password, timeout)
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config RedisCallConfig, log logs.Log) types.Action {
now := time.Now()
minuteAligned := now.Truncate(time.Minute)
timeStamp := strconv.FormatInt(minuteAligned.Unix(), 10)
// err != nil の場合、ゲートウェイは Redis バックエンドに到達できない可能性があります。
// Redis サービスが削除されていないことを確認してください。
err := config.client.Incr(timeStamp, func(response resp.Value) {
if response.Error() != nil {
log.Errorf("call redis error: %v", response.Error())
proxywasm.ResumeHttpRequest()
} else {
ctx.SetContext("timeStamp", timeStamp)
ctx.SetContext("callTimeLeft", strconv.Itoa(config.qpm-response.Integer()))
if response.Integer() == 1 {
err := config.client.Expire(timeStamp, 60, func(response resp.Value) {
if response.Error() != nil {
log.Errorf("call redis error: %v", response.Error())
}
proxywasm.ResumeHttpRequest()
})
if err != nil {
log.Errorf("Error occured while calling redis, it seems cannot find the redis cluster.")
proxywasm.ResumeHttpRequest()
}
} else {
if response.Integer() > config.qpm {
proxywasm.SendHttpResponse(429, [][2]string{{"timeStamp", timeStamp}, {"callTimeLeft", "0"}}, []byte("Too many requests\n"), -1)
} else {
proxywasm.ResumeHttpRequest()
}
}
}
})
if err != nil {
// Redis 呼び出しが失敗した場合は、リクエストの処理を続行し、エラーをログに記録します
log.Errorf("Error occured while calling redis, it seems cannot find the redis cluster.")
return types.HeaderContinue
} else {
// Redis コールバックが完了するまでリクエストを一時停止します
return types.HeaderStopAllIterationAndWatermark
}
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config RedisCallConfig, log logs.Log) types.Action {
if ctx.GetContext("timeStamp") != nil {
proxywasm.AddHttpResponseHeader("timeStamp", ctx.GetContext("timeStamp").(string))
}
if ctx.GetContext("callTimeLeft") != nil {
proxywasm.AddHttpResponseHeader("callTimeLeft", ctx.GetContext("callTimeLeft").(string))
}
return types.HeaderContinue
}