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

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

最終更新日:Nov 09, 2025

ゲートウェイプラグインを開発することで、API ゲートウェイのコア機能を拡張し、複雑で特定のビジネス要件を満たすことができます。このトピックでは、Go を使用してゲートウェイプラグインを開発する方法について説明し、ローカルでの開発とデバッグのガイダンスを提供します。

重要

Higress は、TinyGo 0.29 と Go 1.20 のコンパイルソリューションから、Go 1.24 でサポートされるネイティブの WebAssembly (Wasm) コンパイルに移行しました。Go 1.24 は現在、Wasm ファイルのコンパイルをネイティブでサポートしています。

以前に TinyGo を使用してプラグインをコンパイルし、Go 1.24 のコンパイルモードに移行したい場合は、`go.mod` の依存関係を調整する必要があります。また、プラグインの初期化ロジックを `main` 関数から `init` 関数に移動する必要もあります。具体的な例については、このトピックの後半で提供される情報をご参照ください。

元々 TinyGo で実装されていたプラグインについては、次の互換性調整にご注意ください:

1. ヘッダー処理フェーズ中に外部サービスを呼び出し、`type.ActionPause` を返す場合は、`types.HeaderStopAllIterationAndWatermark` を返すように変更する必要があります。関連する実装については、このトピックの後半にある外部サービスを呼び出すプラグインの例をご参照ください。

2. TinyGo が標準の `regexp` ライブラリを完全にサポートしていないために `go-re2` ライブラリを使用していた場合は、公式の Go `regexp` パッケージに置き換える必要があります。

前提条件

Go をインストールする必要があります。

Golang

詳細については、公式の「インストールガイド」をご参照ください。バージョン 1.24 以降が必要です。

説明

Go 1.24 でコンパイルされたプラグインには、MSE ゲートウェイバージョン 2.0.11 以降が必要です。以前のゲートウェイバージョンについては、「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. wasm-demo-go などのプロジェクトディレクトリを作成します。

  2. ディレクトリ内で、次のコマンドを実行して Go プロジェクトを初期化します。

    go mod init wasm-demo-go
  3. 中国本土にいる場合は、依存関係のダウンロードにプロキシを設定する必要がある場合があります。

    go env -w GOPROXY=https://proxy.golang.com.cn,direct
  4. プラグインをビルドするための依存関係をダウンロードします。

    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

main.go ファイルの作成

次の例は、プラグイン設定で mockEnable: true が設定されている場合に、直接 hello world レスポンスを返す方法を示しています。プラグインが設定されていない場合、または mockEnable: false が設定されている場合、この例では元のリクエストに hello: world ヘッダーを追加します。その他の例については、このトピックの「その他の例」セクションをご参照ください。

説明
ゲートウェイコンソールのプラグイン設定は YAML フォーマットです。プラグインに送信される前に自動的に JSON フォーマットに変換されます。したがって、この例の `parseConfig` 関数は、JSON から直接設定を解析できます。
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
}

// コンソールからの YAML 設定は自動的に JSON に変換されます。json パラメーターから直接設定を解析できます。
func parseConfig(json gjson.Result, config *MyConfig, log logs.Log) error {
  // 設定を解析し、config で更新します。
  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
}

HTTP 処理マウントポイント

前の例では、wrapper.ProcessRequestHeadersBy メソッドを使用して、ユーザー定義関数 onHttpRequestHeadersHTTP リクエストヘッダー処理フェーズに適用しました。次のメソッドを使用して、他のフェーズにユーザー定義関数を設定することもできます。

HTTP 処理フェーズ

トリガー条件

マウント方法

HTTP リクエストヘッダー処理フェーズ

ゲートウェイがクライアントからリクエストヘッダーデータを受信したとき

wrapper.ProcessRequestHeadersBy

HTTP リクエストボディ処理フェーズ

ゲートウェイがクライアントからリクエストボディデータを受信したとき

wrapper.ProcessRequestBodyBy

HTTP レスポンスヘッダー処理フェーズ

ゲートウェイがバックエンドサービスからレスポンスヘッダーデータを受信したとき

wrapper.ProcessResponseHeadersBy

HTTP レスポンスボディ処理フェーズ

ゲートウェイがバックエンドサービスからレスポンスボディデータを受信したとき

wrapper.ProcessResponseBodyBy

ユーティリティメソッド

前の例の proxywasm.AddHttpRequestHeaderproxywasm.SendHttpResponse メソッドは、プラグイン SDK によって提供される 2 つのユーティリティメソッドです。主なユーティリティメソッドを次の表に示します。

カテゴリ

メソッド名

目的

有効な

HTTP 処理フェーズ

リクエストヘッダー処理

GetHttpRequestHeaders

クライアントからすべてのリクエストヘッダーを取得する

HTTP リクエストヘッダー処理フェーズ

ReplaceHttpRequestHeaders

クライアントからすべてのリクエストヘッダーを置き換える

HTTP リクエストヘッダー処理フェーズ

GetHttpRequestHeader

クライアントから特定のリクエストヘッダーを取得する

HTTP リクエストヘッダー処理フェーズ

RemoveHttpRequestHeader

クライアントから特定のリクエストヘッダーを削除する

HTTP リクエストヘッダー処理フェーズ

ReplaceHttpRequestHeader

クライアントから特定のリクエストヘッダーを置き換える

HTTP リクエストヘッダー処理フェーズ

AddHttpRequestHeader

新しいクライアントリクエストヘッダーを追加する

HTTP リクエストヘッダー処理フェーズ

リクエストボディ処理

GetHttpRequestBody

クライアントリクエストボディを取得する

HTTP リクエストボディ処理フェーズ

AppendHttpRequestBody

クライアントリクエストボディの末尾に指定されたバイト文字列を追加する

HTTP リクエストボディ処理フェーズ

PrependHttpRequestBody

クライアントリクエストボディの先頭に指定されたバイト文字列を追加する

HTTP リクエストボディ処理フェーズ

ReplaceHttpRequestBody

クライアントリクエストボディを置き換える

HTTP リクエストボディ処理フェーズ

レスポンスヘッダー処理

GetHttpResponseHeaders

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

HTTP レスポンスヘッダー処理フェーズ

ReplaceHttpResponseHeaders

バックエンドからすべてのレスポンスヘッダーを置き換える

HTTP レスポンスヘッダー処理フェーズ

GetHttpResponseHeader

バックエンドから特定のレスポンスヘッダーを取得する

HTTP レスポンスヘッダー処理フェーズ

RemoveHttpResponseHeader

バックエンドから特定のレスポンスヘッダーを削除する

HTTP レスポンスヘッダー処理フェーズ

ReplaceHttpResponseHeader

バックエンドから特定のレスポンスヘッダーを置き換える

HTTP レスポンスヘッダー処理フェーズ

AddHttpResponseHeader

新しいバックエンドレスポンスヘッダーを追加する

HTTP レスポンスヘッダー処理フェーズ

レスポンスボディ処理

GetHttpResponseBody

クライアントリクエストボディを取得できます。

HTTP レスポンスボディ処理フェーズ

AppendHttpResponseBody

バックエンドレスポンスボディの末尾に指定されたバイト文字列を追加する

HTTP レスポンスボディ処理フェーズ

PrependHttpResponseBody

バックエンドレスポンスボディの先頭に指定されたバイト文字列を追加する

HTTP レスポンスボディ処理フェーズ

ReplaceHttpResponseBody

バックエンドレスポンスボディを置き換える

HTTP レスポンスボディ処理フェーズ

HTTP 呼び出し

DispatchHttpCall

HTTP リクエストを送信する

-

GetHttpCallResponseHeaders

`DispatchHttpCall` リクエストのレスポンスヘッダーを取得する

-

GetHttpCallResponseBody

`DispatchHttpCall` リクエストのレスポンスボディを取得する

-

GetHttpCallResponseTrailers

`DispatchHttpCall` リクエストのレスポンストレーラーを取得する

-

直接レスポンス

SendHttpResponse

特定の HTTP レスポンスを直接返す

-

フロー再開

ResumeHttpRequest

以前に一時停止されたリクエスト処理フローを再開する

-

ResumeHttpResponse

以前に一時停止されたレスポンス処理フローを再開する

-

重要

リクエストまたはレスポンスが一時停止状態でない場合は、`ResumeHttpRequest` または `ResumeHttpResponse` を呼び出さないでください。`SendHttpResponse` が呼び出された後、一時停止されたリクエストまたはレスポンスは自動的に再開されることに注意してください。`ResumeHttpRequest` または `ResumeHttpResponse` を再度呼び出すと、未定義の動作が発生します。

WASM ファイルのコンパイルと生成

wasm ファイルをローカルでコンパイルする

カスタムで初期化されたディレクトリを使用する場合は、次のコマンドを実行して Wasm ファイルをコンパイルします。

go mod tidy
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./

コンパイルが成功すると、`main.wasm` という名前の新しいファイルが生成されます。このファイルは、このトピックの後半にあるローカルデバッグの例で使用されます。クラウドネイティブゲートウェイマーケットプレイスでカスタムプラグイン機能を使用するには、このファイルをアップロードする必要があります。

ヘッダーの状態管理

ヘッダー

説明

HeaderContinue

現在のフィルターの処理が完了し、リクエストを次のフィルターに渡すことができることを示します。types.ActionContinue はこの状態に対応します。

HeaderStopIteration

ヘッダーを次のフィルターにまだ渡すことができないことを示します。ただし、接続からのデータ読み取りは停止されず、ボディデータの処理は引き続きトリガーされます。これにより、ボディデータ処理フェーズ中に HTTP リクエストヘッダーを更新できます。ボディデータを次のフィルターに渡す必要がある場合、ヘッダーも一緒に渡されます。

説明

この状態が返された場合、ボディが必要です。ボディがない場合、リクエストまたはレスポンスは無期限にブロックされます。

リクエストボディが存在するかどうかを確認するには、HasRequestBody() を使用します。

HeaderContinueAndEndStream

ヘッダーは次のフィルターに渡すことができますが、次のフィルターはリクエストがまだ完了していないことを示すために end_stream = false を受け取ります。これにより、現在のフィルターがさらにボディデータを追加できます。

HeaderStopAllIterationAndBuffer

すべての反復を停止します。これは、ヘッダーを次のフィルターに渡すことができず、現在のフィルターがボディデータを受信できないことを示します。現在および後続のフィルターのヘッダー、データ、およびトレーラーはバッファリングされます。バッファーサイズが制限を超えると、リクエストフェーズ中に 413 ステータスコードが返され、レスポンスフェーズ中に 500 ステータスコードが返されます。後続の処理を再開するには、proxywasm.ResumeHttpRequest()proxywasm.ResumeHttpResponse()、または proxywasm.SendHttpResponseWithDetail() を呼び出す必要もあります。

HeaderStopAllIterationAndWatermark

これは HeaderStopAllIterationAndBuffer と同じですが、バッファーが制限を超えるとスロットリングがトリガーされ、接続からのデータ読み取りが一時停止される点が異なります。ABI 0.2.1 の types.ActionPause はこの状態に対応します。

説明

`types.HeaderStopIteration` と `HeaderStopAllIterationAndWatermark` を含むシナリオについては、公式の Higress の「ai-transformer プラグイン」と「ai-quota プラグイン」をご参照ください。

WasmPlugin カスタムリソース定義 (CRD) またはコンソール UI を使用して Higress でこのプラグインを設定するには、Wasm ファイルを OCI または Docker イメージにパッケージ化する必要があります。詳細については、「カスタムプラグイン」をご参照ください。

ローカルデバッグ

前提条件

Docker をインストールする必要があります。

Docker Compose を使用した起動と検証

  1. プラグインを作成したときに作成したディレクトリ、たとえば `wasm-demo` ディレクトリに移動します。このディレクトリに `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: {}
  3. 同じディレクトリに、次の内容で `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
  4. 次のコマンドを実行して Docker Compose を起動します。

    docker compose up

機能の検証

WASM 機能を検証する

  1. curl を使用して httpbin に直接アクセスします。次の例に示すように、リクエストがゲートウェイを通過しない場合のリクエストヘッダーを表示できます。

    curl http://127.0.0.1:12345/get
    
    {
      "args": {},
      "headers": {
        "Accept": "*/*",
        "Host": "127.0.0.1:12345",
        "User-Agent": "curl/7.79.1"
      },
      "origin": "172.18.0.1",
      "url": "http://127.0.0.1:12345/get"
    }
  2. curl を使用してゲートウェイ経由で httpbin にアクセスします。次の例に示すように、ゲートウェイによって処理された後のリクエストヘッダーを表示できます。

    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` リクエストヘッダーが追加されました。

プラグイン設定変更の検証

  1. `envoy.yaml` を変更し、mockEnabletrue に設定します。

      configuration:
        "@type": "type.googleapis.com/google.protobuf.StringValue"
        value: |
          {
            "mockEnable": true
          }
  2. curl を使用してゲートウェイ経由で httpbin にアクセスします。次の例に示すように、ゲートウェイによって処理された後のレスポンスを表示できます。

    curl http://127.0.0.1:10000/get
    
    hello world

これは、プラグイン設定の変更が有効であることを示しています。モックレスポンスが有効になり、`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"
)

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 サービス、および固定 IP アドレスまたは DNS ソースからのサービスにアクセスできます。net/http ライブラリの HTTP クライアントを直接使用することはできず、次の例に示すカプセル化された HTTP クライアントを使用する必要があることに注意してください。

次の例では、設定解析フェーズ中にサービスタイプが解析され、対応する 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
  // リクエスト URL
  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")
  }
  // my-svc.dns、my-svc.static、service-provider.DEFAULT-GROUP.public.nacos、または httpbin.my-ns.svc.cluster.local などのサービスタイプを含む完全な FQDN
  serviceName := json.Get("serviceName").String()
  servicePort := json.Get("servicePort").Int()
  if servicePort == 0 {
    if strings.HasSuffix(serviceName, ".static") {
      // 静的 IP サービスの論理ポートは 80 です
      servicePort = 80
    }
  }
  config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
    FQDN: serviceName,
    Port: servicePort,
        })
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log logs.Log) types.Action {
  // クライアントの Get メソッドを使用して HTTP GET 呼び出しを開始します。ここではタイムアウトパラメーターは省略されており、デフォルトのタイムアウトは 500 ms です。
  err := config.client.Get(config.requestPath, nil,
           // コールバック関数。レスポンスが非同期で返されたときに実行されます
           func(statusCode int, responseHeaders http.Header, responseBody []byte) {
             // リクエストが 200 ステータスコードを返しませんでした。それを処理します。
             if statusCode != http.StatusOK {
               log.Errorf("http call failed, status: %d", statusCode)
               proxywasm.SendHttpResponse(http.StatusInternalServerError, nil,
                 []byte("http call failed"), -1)
               return
             }
             // HTTP ステータスコードとレスポンスボディを出力します
             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 {
    // 非同期コールバックが完了するのを待ちます。HeaderStopAllIterationAndWatermark ステータスを返します。これは ResumeHttpRequest で再開できます。
    return types.HeaderStopAllIterationAndWatermark
  }
}

プラグインから Redis を呼び出す

次のサンプルコードを使用して、Redis レート制限プラグインを実装します。

package main

import (
  "strconv"
  "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 {
  // my-redis.dns や redis.my-ns.svc.cluster.local など、サービスタイプを含む完全な FQDN
  serviceName := json.Get("serviceName").String()
  servicePort := json.Get("servicePort").Int()
  if servicePort == 0 {
    if strings.HasSuffix(serviceName, ".static") {
      // 静的 IP サービスの論理ポートは 80 です
      servicePort = 80
    } else {
      servicePort = 6379
    }
  }
  username := json.Get("username").String()
  password := json.Get("password").String()
  // 単位: ms
  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)
  // Redis API が 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
}