Caddyの拡張
Caddyは、モジュールアーキテクチャにより拡張が容易です。Caddyの拡張機能(またはプラグイン)のほとんどは、Caddyの設定構造を拡張またはプラグインする場合、モジュールとして知られています。明確にするために、CaddyモジュールはGoモジュールとは異なります(ただし、Goモジュールでもあります)。
前提条件
- Caddyのアーキテクチャの基本的な理解
- Go言語の習熟
go
xcaddy
クイックスタート
Caddyモジュールとは、パッケージがインポートされたときにCaddyモジュールとして自身を登録する名前付き型です。重要なのは、モジュールは常にcaddy.Moduleインターフェースを実装することです。このインターフェースは、モジュールの名前とコンストラクタ関数を提供します。
新しいGoモジュールで、次のテンプレートをGoファイルに貼り付けて、パッケージ名、型名、およびCaddyモジュールIDをカスタマイズします。
package mymodule
import "github.com/caddyserver/caddy/v2"
func init() {
caddy.RegisterModule(Gizmo{})
}
// Gizmo is an example; put your own type here.
type Gizmo struct {
}
// CaddyModule returns the Caddy module information.
func (Gizmo) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "foo.gizmo",
New: func() caddy.Module { return new(Gizmo) },
}
}
次に、プロジェクトのディレクトリからこのコマンドを実行すると、リストにモジュールが表示されます。
xcaddy list-modules
...
foo.gizmo
...
おめでとうございます。モジュールがCaddyに登録され、同じ名前空間でモジュールを使用するCaddyの設定ドキュメントで使用できるようになりました。
内部的には、xcaddy
は、Caddyとプラグインの両方(ローカル開発バージョンを使用するための適切なreplace
を使用)を必要とする新しいGoモジュールを作成し、インポートを追加してコンパイルされるようにしています。
import _ "github.com/example/mymodule"
モジュールの基本
Caddyモジュール
- IDとコンストラクタを提供するために
caddy.Module
インターフェースを実装します。 - 適切な名前空間で一意の名前を持つ。
- 通常、その名前空間のホストモジュールにとって意味のあるインターフェースを満たします。
ホストモジュール(または親モジュール)は、他のモジュールを読み込み/初期化するモジュールです。通常、ゲストモジュールの名前空間を定義します。
ゲストモジュール(または*子モジュール*)は、読み込まれたり初期化されたりするモジュールです。すべてのモジュールはゲストモジュールです。
モジュールID
各Caddyモジュールには、名前空間と名前で構成される一意のIDがあります。
- 完全なIDは
foo.bar.module_name
のようになります。 - 名前空間は
foo.bar
になります。 - 名前は、名前空間内で一意である必要がある
module_name
になります。
モジュールIDは、snake_case
規則を使用する必要があります。
名前空間
名前空間はクラスのようなものです。つまり、名前空間は、その中のすべてのモジュールに共通する機能を定義します。たとえば、http.handlers
名前空間内のすべてのモジュールはHTTPハンドラであると期待できます。したがって、ホストモジュールは、その名前空間内のゲストモジュールをinterface{}
型から、caddyhttp.MiddlewareHandler
などのより具体的で有用な型にタイプアサートできます。
ホストモジュールは、特定の名前空間内のモジュールにホストモジュールが望む機能を提供するようにCaddyに要求するため、ゲストモジュールがホストモジュールによって認識されるためには、適切な名前空間が必要です。たとえば、gizmo
というHTTPハンドラモジュールを作成する場合、http
アプリはhttp.handlers
名前空間でハンドラを探すため、モジュールの名前はhttp.handlers.gizmo
になります。
言い換えれば、Caddyモジュールは、モジュール名前空間に応じて特定のインターフェースを実装することが期待されています。この規則により、モジュール開発者は、「`http.handlers`名前空間のすべてのモジュールはHTTPハンドラです」など、直感的なことを言うことができます。より技術的には、これは通常、「`http.handlers`名前空間のすべてのモジュールは`caddyhttp.MiddlewareHandler`インターフェースを実装します」を意味します。そのメソッドセットがわかっているため、より具体的な型をアサートして使用できます。
すべての標準Caddy名前空間をGo型にマッピングする表を表示します。
caddy
およびadmin
名前空間は予約されており、アプリ名として使用できません。
サードパーティのホストモジュールにプラグインするモジュールを作成するには、それらのモジュールの名前空間のドキュメントを参照してください。
名前
名前空間内の名前は重要であり、ユーザーには非常によく見えますが、一意で簡潔であり、その機能にとって意味がある限り、特に重要ではありません。
アプリモジュール
アプリは、空の名前空間を持つモジュールであり、慣例により独自のトップレベル名前空間になります。アプリモジュールはcaddy.Appインターフェースを実装します。
これらのモジュールは、Caddyの設定のトップレベルの"apps"
プロパティに表示されます。
{
"apps": {}
}
アプリの例は、http
とtls
です。これらの名前空間は空です。
これらのアプリ用に作成されたゲストモジュールは、アプリ名から派生した名前空間に配置する必要があります。たとえば、HTTPハンドラはhttp.handlers
名前空間を使用し、TLS証明書ローダーはtls.certificates
名前空間を使用します。
モジュールの実装
モジュールは事実上あらゆる型にすることができますが、ユーザー設定を保持できるため、構造体が最も一般的です。
設定
ほとんどのモジュールには、何らかの設定が必要です。型がJSONと互換性がある限り、Caddyはこれを自動的に処理します。したがって、モジュールが構造体型である場合、そのフィールドに構造体タグが必要になります。これは、Caddyの規則に従って`snake_casing`を使用する必要があります。
type Gizmo struct {
MyField string `json:"my_field,omitempty"`
Number int `json:"number,omitempty"`
}
このように構造体タグを使用すると、Caddy全体で設定プロパティの名前が統一されます。
モジュールが初期化されると、設定はすでに完了しています。モジュールの初期化後に、追加のプロビジョニングと検証ステップを実行することもできます。
モジュールのライフサイクル
モジュールのライフは、ホストモジュールによってロードされると開始されます。以下が発生します。
- モジュールの値のインスタンスを取得するために、
New()
が呼び出されます。 - モジュールの設定がそのインスタンスにアンマーシャリングされます。
- モジュールがcaddy.Provisionerの場合、`Provision()`メソッドが呼び出されます。
- モジュールがcaddy.Validatorの場合、`Validate()`メソッドが呼び出されます。
- この時点で、ホストモジュールにはロードされたゲストモジュールが`interface{}`値として渡されるため、ホストモジュールは通常、ゲストモジュールをより有用な型にタイプアサートします。名前空間のゲストモジュールに必要なもの、たとえば実装する必要があるメソッドを知るには、ホストモジュールのドキュメントを確認してください。
- モジュールが不要になった場合、モジュールがcaddy.CleanerUpperの場合、`Cleanup()`メソッドが呼び出されます。
モジュールの複数のロードされたインスタンスが、特定の時間に重複する可能性があることに注意してください。設定の変更中は、古いモジュールが停止する前に新しいモジュールが起動されます。グローバル状態は慎重に使用してください。caddy.UsagePool型を使用して、モジュールロード全体のグローバル状態の管理に役立てます。モジュールがソケットでリッスンする場合は、`caddy.Listen*()`を使用して、重複使用をサポートするソケットを取得します。
プロビジョニング
モジュールの設定は、その値に自動的にアンマーシャリングされます。これは、たとえば、構造体フィールドが自動的に設定されることを意味します。
ただし、モジュールに追加のプロビジョニングステップが必要な場合は、(オプションの)caddy.Provisionerインターフェースを実装できます。
// Provision sets up the module.
func (g *Gizmo) Provision(ctx caddy.Context) error {
// TODO: set up the module
return nil
}
これは通常、ホストモジュールがゲスト/子モジュールをロードする場所ですが、ほとんどすべての用途に使用できます。モジュールプロビジョニングは任意の順序で実行されます。
モジュールは`ctx.App()`を呼び出すことによって他のアプリにアクセスできますが、モジュールは循環依存関係を持つことはできません。言い換えれば、`http`アプリによってロードされたモジュールは、`tls`アプリによってロードされたモジュールが`http`アプリに依存する場合、`tls`アプリに依存することはできません。(Goでのインポートサイクルを禁止するルールと非常によく似ています。)
さらに、プロビジョニングは設定が検証されているだけでも実行されるため、`Provision`で負荷の高い操作を実行することは避ける必要があります。プロビジョニングフェーズでは、モジュールが実際に使用されるとは想定しないでください。
ログ
Caddyのロギングの仕組みを参照してください。モジュールにロギングが必要な場合は、Go標準ライブラリの`log.Print*()`を使用しないでください。言い換えれば、**Goのグローバルロガーを使用しないでください**。Caddyは、zapを使用した高性能で非常に柔軟な構造化ロギングを使用します。
ログを出力するには、モジュールのProvisionメソッドでロガーを取得します。
func (g *Gizmo) Provision(ctx caddy.Context) error {
g.logger = ctx.Logger() // g.logger is a *zap.Logger
}
次に、`g.logger`を使用して、構造化されたレベル付きログを出力できます。詳細については、zapのgodocを参照してください。
検証
設定の検証を行いたいモジュールは、(オプションの)caddy.Validator
インターフェースを実装することで実現できます。
// Validate validates that the module has a usable config.
func (g Gizmo) Validate() error {
// TODO: validate the module's setup
return nil
}
`Validate` は読み取り専用の関数であるべきです。この関数は `Provision()` メソッドの後に実行されます。
インターフェースガード
Go のインターフェースは暗黙的に満たされるため、Caddy モジュールの動作は暗黙的です。モジュールの型に適切なメソッドを追加するだけで、モジュールの正誤が決まります。そのため、タイプミスやメソッドシグネチャの間違いは、予期しない動作(動作の欠如)につながる可能性があります。
幸いなことに、適切なメソッドが追加されていることを確認するために、コードに簡単に追加できる、オーバーヘッドのないコンパイル時チェックがあります。これらはインターフェースガードと呼ばれます。
var _ InterfaceName = (*YourType)(nil)
`InterfaceName` を満たそうとするインターフェースに、`YourType` をモジュールの型の名前で置き換えます。
たとえば、静的ファイルサーバーなどの HTTP ハンドラーは、複数のインターフェースを満たす場合があります。
// Interface guards
var (
_ caddy.Provisioner = (*FileServer)(nil)
_ caddyhttp.MiddlewareHandler = (*FileServer)(nil)
)
これにより、`*FileServer` がこれらのインターフェースを満たしていない場合、プログラムのコンパイルが防止されます。
インターフェースガードがないと、混乱を招くバグが入り込む可能性があります。たとえば、モジュールを使用する前にプロビジョニングする必要があるが、`Provision()` メソッドに誤り(スペルミスやシグネチャの間違いなど)があると、プロビジョニングは実行されず、原因究明に時間がかかります。インターフェースガードは非常に簡単で、それを防ぐことができます。通常、ファイルの最後に配置します。
ホストモジュール
モジュールは、独自のゲストモジュールを読み込むと、ホストモジュールになります。これは、モジュールの機能の一部をさまざまな方法で実装できる場合に便利です。
ホストモジュールは、ほとんどの場合構造体です。通常、ゲストモジュールをサポートするには、2つの構造体フィールドが必要です。1つは生の JSON を保持するため、もう1つはデコードされた値を保持するためのものです。
type Gizmo struct {
GadgetRaw json.RawMessage `json:"gadget,omitempty" caddy:"namespace=foo.gizmo.gadgets inline_key=gadgeter"`
Gadget Gadgeter `json:"-"`
}
最初のフィールド(この例では `GadgetRaw`)は、ゲストモジュールの生の、プロビジョニングされていない JSON 形式が見つかる場所です。
2番目のフィールド(`Gadget`)は、最終的なプロビジョニングされた値が最終的に格納される場所です。2番目のフィールドはユーザー向けではないため、構造体タグを使用して JSON から除外します。(他のパッケージで必要ない場合は、エクスポートしないこともできます。その場合、構造体タグは必要ありません。)
Caddy 構造体タグ
生のモジュールフィールドの `caddy` 構造体タグは、Caddy が読み込むモジュールの名前空間と名前(完全な ID を構成する)を知るのに役立ちます。また、ドキュメントの生成にも使用されます。
構造体タグの形式は非常にシンプルです:`key1=val1 key2=val2 ...`
モジュールフィールドの場合、構造体タグは次のようになります。
`caddy:"namespace=foo.bar inline_key=baz"`
`namespace=` 部分は必須です。モジュールを探す名前空間を定義します。
`inline_key=` 部分は、モジュールの名前がモジュール自体と*インライン*で見つかる場合にのみ使用されます。これは、値がオブジェクトであり、キーの1つが*インラインキー*であり、その値がモジュールの名前であることを意味します。省略した場合、フィールドタイプは caddy.ModuleMap
または `[]caddy.ModuleMap` である必要があります。ここで、マップキーはモジュール名です。
ゲストモジュールの読み込み
ゲストモジュールを読み込むには、プロビジョニングフェーズ中に ctx.LoadModule()
を呼び出します。
// Provision sets up g and loads its gadget.
func (g *Gizmo) Provision(ctx caddy.Context) error {
if g.GadgetRaw != nil {
val, err := ctx.LoadModule(g, "GadgetRaw")
if err != nil {
return fmt.Errorf("loading gadget module: %v", err)
}
g.Gadget = val.(Gadgeter)
}
return nil
}
`LoadModule()` 呼び出しは、構造体へのポインタとフィールド名を文字列として受け取ります。奇妙ですよね?なぜ構造体フィールドを直接渡さないのでしょうか?これは、設定のレイアウトに応じて、モジュールを読み込む方法がいくつかあるためです。このメソッドシグネチャにより、Caddy はリフレクションを使用して、モジュールを読み込む最適な方法、そして最も重要なこととして、その構造体タグを読み取る方法を理解できます。
ゲストモジュールをユーザーが明示的に設定する必要がある場合は、読み込もうとする前に Raw フィールドが nil または空の場合、エラーを返す必要があります。
読み込まれたモジュールが型アサーションされていることに注目してください:`g.Gadget = val.(Gadgeter)` - これは、返された `val` が `interface{}` 型であり、あまり役に立たないためです。ただし、宣言された名前空間(この例の構造体タグからの `foo.gizmo.gadgets`)のすべてのモジュールが `Gadgeter` インターフェースを実装することを期待しているため、この型アサーションは安全であり、使用できます!
ホストモジュールが新しい名前空間を定義する場合は、ここで行ったように、開発者向けにその名前空間とその Go タイプを必ず文書化してください。
モジュールのドキュメント
新しい Caddy モジュールをモジュールのドキュメントに表示し、https://caddy.dokyumento.jp/download で使用できるようにするには、モジュールを登録します。登録は https://caddy.dokyumento.jp/account で利用できます。アカウントをお持ちでない場合は、新規アカウントを作成し、「パッケージの登録」をクリックしてください。
完全な例
HTTP ハンドラーモジュールを作成したいとしましょう。これは、デモンストレーションの目的で、すべての HTTP リクエストで訪問者の IP アドレスをストリームに出力する、人為的なミドルウェアになります。
また、ほとんどの人は自動化されていない状況で Caddyfile を使用することを好むため、Caddyfile を介して設定できるようにしたいと考えています。これは、HTTP ルートにハンドラーを追加できる一種のディレクティブである Caddyfile ハンドラーディレクティブを登録することで行います。また、`caddyfile.Unmarshaler` インターフェースも実装します。これらの数行のコードを追加することで、このモジュールは Caddyfile で設定できます!たとえば:`visitor_ip stdout`。
これは、そのようなモジュールのコードで、説明的なコメントが付いています。
package visitorip
import (
"fmt"
"io"
"net/http"
"os"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func init() {
caddy.RegisterModule(Middleware{})
httpcaddyfile.RegisterHandlerDirective("visitor_ip", parseCaddyfile)
}
// Middleware implements an HTTP handler that writes the
// visitor's IP address to a file or stream.
type Middleware struct {
// The file or stream to write to. Can be "stdout"
// or "stderr".
Output string `json:"output,omitempty"`
w io.Writer
}
// CaddyModule returns the Caddy module information.
func (Middleware) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.visitor_ip",
New: func() caddy.Module { return new(Middleware) },
}
}
// Provision implements caddy.Provisioner.
func (m *Middleware) Provision(ctx caddy.Context) error {
switch m.Output {
case "stdout":
m.w = os.Stdout
case "stderr":
m.w = os.Stderr
default:
return fmt.Errorf("an output stream is required")
}
return nil
}
// Validate implements caddy.Validator.
func (m *Middleware) Validate() error {
if m.w == nil {
return fmt.Errorf("no writer")
}
return nil
}
// ServeHTTP implements caddyhttp.MiddlewareHandler.
func (m Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
m.w.Write([]byte(r.RemoteAddr))
return next.ServeHTTP(w, r)
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume directive name
// require an argument
if !d.NextArg() {
return d.ArgErr()
}
// store the argument
m.Output = d.Val()
return nil
}
// parseCaddyfile unmarshals tokens from h into a new Middleware.
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var m Middleware
err := m.UnmarshalCaddyfile(h.Dispenser)
return m, err
}
// Interface guards
var (
_ caddy.Provisioner = (*Middleware)(nil)
_ caddy.Validator = (*Middleware)(nil)
_ caddyhttp.MiddlewareHandler = (*Middleware)(nil)
_ caddyfile.Unmarshaler = (*Middleware)(nil)
)