↪️

traefikのミドルウェアプラグインを自作する(Go)

2024/09/13に公開1

https://zenn.dev/hiiraginil/scraps/e5bc24889958b3 を校正したりしたもの

traefik/Go

traefik, Goについての説明は、本記事に辿り着いた時点でお分かりだと思うので省略。
余談だが、traefik自体Goで作られている。

Goでプラグインを自作したときにできること。

  • Goの型システムの上で開発ができる。標準ライブラリに則っているので独自ルールも最小限
  • 複雑なミドルウェア(〇〇の場合だけ××するとか、JSONの加工とか)を記述できる
  • 楽しい

公式のガイドは以下。 Goだけでなく、http-wasm を使用して開発することもできる。
https://plugins.traefik.io/create

traefik自体にGoのインタプリタ(Yaegi)が同梱されており、プラグインの実行にもこれを用いる。

Goのミドルウェアプラグインのサンプル兼テンプレートレポジトリは以下。
https://github.com/traefik/plugindemo
基本的にこのテンプレートレポジトリを使用して開発することになる。

技術周り

前述したとおり、traefikにはYaegiと呼ばれるGoインタプリタが同梱されている(ちなみにtraefik製)
Goプラグインの実行時には、Yaegiを使ってプラグインコードを動的に実行することでミドルウェアとして機能させている。
そのため、外部モジュールを使用できないなどの制約が存在する。

仕様理解

まずはテンプレートレポジトリからレポジトリを作成し、go.mod などを変更する。
ちなみに、この時にnamed importせずに済む名前にしておくと後で非常に楽になる

実装について

設定部分

https://github.com/traefik/plugindemo/blob/8a77aea29f9038903ab44059e2aa42a37ff52752/demo.go#L12-L23

設定項目と、それに付随する処理。
jsonメタタグが設定されているがあまり気にしないで良い。
以下の制約がある。

  • 外部から参照されるため、必要なフィールド大文字である必要がある。
    • これはjson.Unmarshal の都合もあるが、Yaegiから直接構造体を注入されることもあるため
  • 外部から実行されるため、CreateConfig() 関数が必須。
  • 同じ理由で、構造体もConfig という名前である必要がある。

構造体のネストとかできるのかな...? map[string]string とか使っている感じいけそう。
chan とかpointer以外の型ならいけそうな予感がする。

初期化部分

https://github.com/traefik/plugindemo/blob/8a77aea29f9038903ab44059e2aa42a37ff52752/demo.go#L25-L45

プラグインの初期化処理と、内部に保持するフィールド。
Demo 構造体はあくまでサンプルなので、実際の実装は実装者による...が、next http.Handlername string と設定項目cfg *Configは大体共通なんじゃないだろうか。
自分の場合は、Config は直接保持することにしている。
https://github.com/FlowingSPDG/traefik-plugin-query-to-json/blob/master/querytojson.go#L27-L33

ちなみに、使わない引数があったとしてもNew(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) というシグネチャは変更してはいけない。
Yaegi側でCreateConfig()を実行し、その結果が渡されるため、例えば仮にctxを使用しないとしてもシグネチャ自体は同じである必要がある。

traefik/Yaegi側の実行イメージ

cfg := your_middleware.CreateConfig()
// このあたりでcfg変数のデータにymlやtestDataを入れているような気がする

ctx := context.Background()
mw, err := your_middleware.New(ctx, next, cfg, "your_plugin_on_traefik")
if err != nil {
  return err
}

// mw.ServeHTTP...

実処理部分

https://github.com/traefik/plugindemo/blob/8a77aea29f9038903ab44059e2aa42a37ff52752/demo.go#L46-L66

ServeHTTP(rw http.ResponseWriter, req *http.Request) が実際の処理。
これ自体はGoのHTTPミドルウェアでよく見る形で、http.Handler interfaceを実装している。
引数のrw, req を使用して前/後処理をいれる。

その他設定

設定用のファイルは、ドキュメント上ではManifestと呼ばれる。
ファイル実態としては .traefik.yml。 初期から設定されている。
あえて公式ではなく、自作したミドルウェアの設定項目とmanifestを見比べてみる。

manifest
https://github.com/FlowingSPDG/traefik-plugin-query-to-json/blob/master/.traefik.yml#L1-L12

config
https://github.com/FlowingSPDG/traefik-plugin-query-to-json/blob/637cd974a92cac259c59da28f20ab86a13833ec8/querytojson.go#L16-L18

各種フィールドについて

  • displayName 表示名。公開するときに設定されるっぽい
  • type middleware 固定でOK。
  • iconPath 公開時に使用されるアイコンファイルへのパス
  • import Goのpackageとしてのimportパス。go.mod と同じでOKのはず
  • basePkg オプション。今回僕のレポジトリ名はtraefik-plugin-query-to-json だが、Goのpackage名としては適切ではなかったのでquerytojsonとしてimportされる必要があったので設定した
  • summary プラグインの説明。
  • testData 結構大事。 プラグインとして公開前にこのフィールドのデータでテスト実行される。 また、Yaegiから直接構造体のフィールドにデータが挿入されるのでフィールド名を直接記載する

実装してみる

実際に作業をしたレポジトリはこちら。

https://github.com/FlowingSPDG/traefik-plugin-logger

セットアップ

まずはセットアップから。
現時点でgolangci-lint などの設定が古いので更新する。
また、go.mod などの設定も、traefik/plugindemo からレポジトリ名に変更する。

今回は簡易的なロガーを作成するため、設定には以下を採用する。

  • ログレベル: ([debug|info|warn|error|]
  • Prefix: string

レポジトリのlogはこちら。
https://github.com/FlowingSPDG/traefik-plugin-logger/commit/57f5aa9d0a59fc1efd3ef1a081de2c30d6e12dc8

また、ログ出力を行う関係上、slogパッケージを採用する。

実装

configファイル中のログレベルはstringで扱いたいため、変換メソッドを用意する。
(今思うと、これはメソッド化せずNew の時に変換し、slog.Level を直接保持しても良かったかも?)

// logLevel stringのLogLevelをslog.Level に変換する
func (c *Config) logLevel() slog.Level {
	switch c.LogLevel {
	case "debug":
		return slog.LevelDebug
	case "info":
		return slog.LevelInfo
	case "warn":
		return slog.LevelDebug
	case "error":
		return slog.LevelError
	default:
		return slog.LevelInfo
	}
}

プラグインの本体に*slog.Logger を保持するフィールドを追加する。

type Logger struct {
	// next, name はほぼ必須フィールド
	next http.Handler
	name string

	// 設定
	cfg *Config

	// ログの出力先
	logger *slog.Logger
}

まずはリクエストを[]slog.Attrs に変換する関数を作成する。

func convertRequest(req *http.Request) []slog.Attr {
	return []slog.Attr{
		slog.String("proto", req.Proto),
		slog.String("method", req.Method),
		slog.String("url", req.URL.String()),
	}
}

ミドルウェアの処理に追加する

// ServeHTTP はミドルウェアの実処理
func (l *Logger) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	// contextを取得
	ctx := req.Context()

	// fieldに変換
	field := convertRequest(req)

	// ログを出力
	l.logger.LogAttrs(ctx, l.cfg.logLevel(), l.cfg.Prefix, field...)

	// next...
	l.next.ServeHTTP(rw, req)

	// 後処理
}

終わり。

ローカルで動かしてみる

TODO.

公開

TODO.(2)

実際に使ってみる

TODO.(3)

Discussion