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

API Gateway: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 をインストールします。

Go

公式インストールガイド (バージョン 1.24 以降が必要です)。

説明

Go 1.24 でコンパイルされたプラグインには、クラウドネイティブ API ゲートウェイのバージョン 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. プロジェクトディレクトリ (例: 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 ヘッダーが追加されます。その他の例については、この Topic の「その他の例」セクションをご参照ください。

説明
ゲートウェイコンソールのプラグイン設定は 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.AddHttpRequestHeader および proxywasm.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")
  }
  // サービスタイプを含む完全な 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 サービスの論理ポートは 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 {
  // サービスタイプを含む完全な 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") {
      // 静的 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
}