traefikのミドルウェアプラグインを自作する(Go)
https://zenn.dev/hiiraginil/scraps/e5bc24889958b3 を校正したりしたもの
traefik/Go
traefik, Goについての説明は、本記事に辿り着いた時点でお分かりだと思うので省略。
余談だが、traefik自体Goで作られている。
Goでプラグインを自作したときにできること。
- Goの型システムの上で開発ができる。標準ライブラリに則っているので独自ルールも最小限
- 複雑なミドルウェア(〇〇の場合だけ××するとか、JSONの加工とか)を記述できる
- 楽しい
公式のガイドは以下。 Goだけでなく、http-wasm を使用して開発することもできる。
traefik自体にGoのインタプリタ(Yaegi)が同梱されており、プラグインの実行にもこれを用いる。
Goのミドルウェアプラグインのサンプル兼テンプレートレポジトリは以下。
基本的にこのテンプレートレポジトリを使用して開発することになる。技術周り
前述したとおり、traefikにはYaegiと呼ばれるGoインタプリタが同梱されている(ちなみにtraefik製)
Goプラグインの実行時には、Yaegiを使ってプラグインコードを動的に実行することでミドルウェアとして機能させている。
そのため、外部モジュールを使用できないなどの制約が存在する。
仕様理解
まずはテンプレートレポジトリからレポジトリを作成し、go.mod
などを変更する。
ちなみに、この時にnamed importせずに済む名前にしておくと後で非常に楽になる。
実装について
設定部分
設定項目と、それに付随する処理。
json
メタタグが設定されているがあまり気にしないで良い。
以下の制約がある。
- 外部から参照されるため、必要なフィールド大文字である必要がある。
- これは
json.Unmarshal
の都合もあるが、Yaegiから直接構造体を注入されることもあるため
- これは
- 外部から実行されるため、
CreateConfig()
関数が必須。 - 同じ理由で、構造体も
Config
という名前である必要がある。
構造体のネストとかできるのかな...? map[string]string
とか使っている感じいけそう。
chan
とかpointer
以外の型ならいけそうな予感がする。
初期化部分
プラグインの初期化処理と、内部に保持するフィールド。
Demo
構造体はあくまでサンプルなので、実際の実装は実装者による...が、next http.Handler
とname string
と設定項目cfg *Config
は大体共通なんじゃないだろうか。
自分の場合は、Config
は直接保持することにしている。
ちなみに、使わない引数があったとしても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...
実処理部分
ServeHTTP(rw http.ResponseWriter, req *http.Request)
が実際の処理。
これ自体はGoのHTTPミドルウェアでよく見る形で、http.Handler
interfaceを実装している。
引数のrw
, req
を使用して前/後処理をいれる。
その他設定
設定用のファイルは、ドキュメント上ではManifest
と呼ばれる。
ファイル実態としては .traefik.yml
。 初期から設定されている。
あえて公式ではなく、自作したミドルウェアの設定項目とmanifestを見比べてみる。
manifest
config
各種フィールドについて
-
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から直接構造体のフィールドにデータが挿入されるのでフィールド名を直接記載する
実装してみる
実際に作業をしたレポジトリはこちら。
セットアップ
まずはセットアップから。
現時点でgolangci-lint
などの設定が古いので更新する。
また、go.mod
などの設定も、traefik/plugindemo
からレポジトリ名に変更する。
今回は簡易的なロガーを作成するため、設定には以下を採用する。
- ログレベル: ([debug|info|warn|error|]
- Prefix: string
レポジトリのlogはこちら。
また、ログ出力を行う関係上、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
テンプレートレポジトリのツール群が古いことに関しては、PRを作成してある。
マージされたら本記事も更新する。