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

API Gateway:Go によるゲートウェイプラグインの開発

最終更新日:Aug 27, 2026

Go を使用してカスタムゲートウェイプラグインを開発し、ローカルでデバッグすることで、複雑なビジネス要件に合わせて API ゲートウェイの機能を拡張します。

重要

Higress は、TinyGo 0.29 + Go 1.20 から、Go 1.24 での Wasm のネイティブコンパイルに移行しました。Go 1.24 は、.wasm ファイルのコンパイルをネイティブでサポートしています。

TinyGo から Go 1.24 に移行するには、プラグインの初期化ロジックを main 関数から init 関数に移動し、go.mod の依存関係を更新する必要があります。以下に例を示します。

TinyGo ベースのプラグインを移行するための互換性調整:

1. ヘッダー処理中に外部サービスを呼び出し、type.ActionPause を返す場合は、types.HeaderStopAllIterationAndWatermark に置き換えてください。この方法は、後述の「外部 HTTP サービスの呼び出し」の例で説明しています。

2. go-re2 (TinyGo の regexp サポートが不完全なため) を使用していた場合は、標準の Go regexp パッケージに置き換えてください。

前提条件

開発マシンに Go 1.24 以降がインストールされている必要があります。

Golang

公式のインストールガイドに従って Go 1.24 以降をインストールしてください。

説明

Go 1.24 プラグインには、Cloud-native API Gateway 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 設定を解析して設定構造体に格納します。
// ゲートウェイコンソールは 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 ファイルが生成されます。このファイルを使用してローカルデバッグを行うか、Cloud-native API Gateway のマーケットプレイスを通じてアップロードし、カスタムプラグインをデプロイします。

WasmPlugin カスタムリソース定義 (CRD) またはコンソール UI を使用して Higress 経由でデプロイするには、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

モックレスポンスは、設定変更が正しく反映されていることを示します。

その他の例

設定不要のプラグイン

設定が不要なプラグインの場合、空の設定構造体を定義して、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
}