すべてのプロダクト
Search
ドキュメントセンター

API Gateway:Go を使用したゲートウェイプラグインの開発

最終更新日:Aug 27, 2026

カスタム 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/bin
    • go version を実行してインストールを確認してください。

プラグインの作成

プロジェクトの初期化

  1. プロジェクトディレクトリを作成し、Go モジュールを初期化します:

       mkdir wasm-demo-go && cd wasm-demo-go
       go mod init wasm-demo-go
  2. プラグイン 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 メソッドを提供します:

リクエストヘッダーの処理 (リクエストヘッダーフェーズで有効)

メソッド

目的

GetHttpRequestHeaders

すべてのリクエストヘッダーを取得

ReplaceHttpRequestHeaders

すべてのリクエストヘッダーを置換

GetHttpRequestHeader

特定のリクエストヘッダーを取得

RemoveHttpRequestHeader

特定のリクエストヘッダーを削除

ReplaceHttpRequestHeader

特定のリクエストヘッダーを置換

AddHttpRequestHeader

リクエストヘッダーを追加

リクエストボディの処理 (リクエストボディフェーズで有効)

メソッド

目的

GetHttpRequestBody

リクエストボディを取得

AppendHttpRequestBody

リクエストボディの末尾にデータを追加

PrependHttpRequestBody

リクエストボディの先頭にデータを追加

ReplaceHttpRequestBody

リクエストボディ全体を置換

レスポンスヘッダーの処理 (レスポンスヘッダーフェーズで有効)

メソッド

目的

GetHttpResponseHeaders

バックエンドからすべてのレスポンスヘッダーを取得

ReplaceHttpResponseHeaders

すべてのレスポンスヘッダーを置換

GetHttpResponseHeader

特定のレスポンスヘッダーを取得

RemoveHttpResponseHeader

特定のレスポンスヘッダーを削除

ReplaceHttpResponseHeader

特定のレスポンスヘッダーを置換

AddHttpResponseHeader

レスポンスヘッダーを追加

レスポンスボディの処理 (レスポンスボディフェーズで有効)

メソッド

目的

GetHttpResponseBody

レスポンスボディを取得

AppendHttpResponseBody

レスポンスボディの末尾にデータを追加

PrependHttpResponseBody

レスポンスボディの先頭にデータを追加

ReplaceHttpResponseBody

レスポンスボディ全体を置換

HTTP 呼び出しとフロー制御

メソッド

目的

DispatchHttpCall

外部サービスへ HTTP リクエストを送信

GetHttpCallResponseHeaders

DispatchHttpCall リクエストからのレスポンスヘッダーを取得

GetHttpCallResponseBody

DispatchHttpCall リクエストからのレスポンスボディを取得

GetHttpCallResponseTrailers

DispatchHttpCall リクエストからのレスポンストレーラーを取得

SendHttpResponse

クライアントに直接 HTTP レスポンスを返す

ResumeHttpRequest

一時停止したリクエスト処理フローを再開

ResumeHttpResponse

一時停止したレスポンス処理フローを再開

重要

処理が一時停止していない場合は、ResumeHttpRequest または ResumeHttpResponse を呼び出さないでください。SendHttpResponse が呼び出されると、一時停止状態が解消され、クライアントに直接レスポンスが送信されます。そのため、再度 ResumeHttpRequest または ResumeHttpResponse を呼び出すと、未定義の動作が発生します。

ヘッダーのステータスコード

各処理フックは、リクエストフローを制御するヘッダーのステータスコードを返します。プラグインがデータを一時停止、バッファリング、またはストリーミングする必要があるかどうかに基づいて、適切なステータスを選択してください:

ステータス

動作

HeaderContinue

現在のフィルター処理を完了し、リクエストを次のフィルターに渡します。types.ActionContinue と同等です。

HeaderStopIteration

ヘッダーを保持しますが、ボディデータの読み取りは続行します。ボディ処理フェーズ中にリクエストヘッダーを変更する場合に使用します。ボディが必要です -- ボディが存在しない場合、リクエストは無期限にブロックされます。HasRequestBody() で確認してください。

HeaderContinueAndEndStream

ヘッダーを end_stream = false で次のフィルターに渡し、現在のフィルターがさらにボディデータを追加できるようにします。

HeaderStopAllIterationAndBuffer

すべての反復を停止し、ヘッダー、ボディ、およびトレーラーをバッファリングします。バッファが制限を超えた場合、ゲートウェイはリクエストフェーズ中に 413、レスポンスフェーズ中に 500 を返します。proxywasm.ResumeHttpRequest()、proxywasm.ResumeHttpResponse()、または proxywasm.SendHttpResponseWithDetail() で再開してください。

HeaderStopAllIterationAndWatermark

HeaderStopAllIterationAndBuffer と同じですが、バッファが制限を超えた場合にエラーを返す代わりに、接続レベルのスロットリングをトリガーします。ABI 0.2.1 の types.ActionPause と同等です。

説明

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
}