ストイックに生きたい
CoreDNS の仕組みとプラグイン
CoreDNS がどうやって動いているかをプラグインをもとに調べた
このエントリーをはてなブックマークに追加

概要

CoreDNS は、Go で実装された DNS サーバーであり、Kubernetes の DNS サーバーとしても利用されている。
特徴的な仕組みとして、プラグインという仕組みがあり、殆どの機能はプラグインで実装されており、プラグインを自分で実装することも可能である。

CoreDNS のローカルでのビルドおよび実行は、簡単に行うことができる。
具体的には、下記のように make コマンドを実行することで、ビルドを行える。

$ git clone https://github.com/coredns/coredns.git
$ cd coredns
$ make

ビルドが完了すると、coredns というバイナリが生成されるので、実行することで CoreDNS が起動する。

$ ./coredns
maxprocs: Leaving GOMAXPROCS=12: CPU quota undefined
.:53
CoreDNS-1.12.0
darwin/amd64, go1.23.3, 177253340

基礎

CoreDNS では、Corefile という設定ファイルを読み込み、その設定に従って DNS サーバーを起動する。

$ cat Corefile
.:10053 {
    forward . 1.1.1.1
    log
    errors
}

$ ./coredns
maxprocs: Leaving GOMAXPROCS=12: CPU quota undefined
.:10053
CoreDNS-1.12.0
darwin/amd64, go1.23.3, 177253340
[INFO] 127.0.0.1:63479 - 32124 "A IN example.com. udp 40 false 4096" NOERROR qr,rd,ra,ad 67 0.005239353s

上記のような Corefile を用意することで、下記のような動作を行うようになる。
errors プラグインが有効でない場合、デフォルトではエラーログは出力されないので、エラーを確認したいなら有効にしましょう。

もし、Corefile が存在しない場合は、デフォルトの設定で起動するようになっており、whoamilog プラグインが有効になっている。

	caddy.RegisterServerType(serverType, caddy.ServerType{
		Directives: func() []string { return Directives },
		DefaultInput: func() caddy.Input {
			return caddy.CaddyfileInput{
				Filepath:       "Corefile",
				Contents:       []byte(".:" + Port + " {\nwhoami\nlog\n}\n"),
				ServerTypeName: serverType,
			}
		},
		NewContext: newContext,
	})

Corefile

Corefile は、Caddyfile と同じようにパースされるものとなっており、環境変数等を使用することが可能である。
なお、内部的に使用している coredns/caddycaddyserver/caddy の v1 のフォークであるため、v2 の機能を利用することはできない。

forward . {$FORWARDS}
$ FORWARDS=1.1.1.1 ./coredns

また、複数のゾーンを指定することができ、それぞれのゾーンに対して別の設定を行うこともできる。
たとえば、example.net では hosts プラグインを有効にし、それ以外では forward プラグインを有効にするような設定を行うことができる。

.:10053 {
    forward . 1.1.1.1
    log
    errors
}

example.net:10053 {
    hosts {
        127.0.0.1 example.net
    }
    log
    errors
}

プラグインの仕組み

Corefile でデフォルトで使うことができるプラグインは、Plugins から確認することができる。
これらのプラグインは、plugin に実装があり、plugin.cfg に定義されているプラグインがビルド時に追加されるようになっている。

また、External Plugins に記載されているプラグインは、plugin.cfg に定義されていない外部のプラグインとなるため、自分で追記した上でビルドする必要がある。

では、具体的にどのように plugin.cfg に定義されているプラグインがビルド時に追加されるかというと、go generate コマンドを実行することで、plugin.go で定義されている directives_generate.go が plugin.cfg を読み込んでコードを生成するようになっている。
その結果、 core/dnsserver/zdirectives.go および core/plugin/zplugin.go が生成されて、プラグインを使用することができるようになる。

プラグインの順番

Configuration に記載があるとおり、各プラグインが実行される順番は Corefile の順番ではなく、plugin.cfg に定義されている順番で実行される。
たとえば、cache プラグインが使用している場合、キャッシュが存在すれば、それ以降のプラグインは実行されない。
このため、Corefile に記載するプラグインの順番を気にする必要はない。

プラグインの追加

サンプルプラグインとして、example が提供されているので、これを使ってプラグインの追加を試すことができる。
plugin.cfg に下記のように追加し、make コマンドを実行することで、example プラグインが追加された状態でビルドされる。
また、前述したとおり、plugin.cfg に定義されているプラグインの順番は重要なため、今回の場合、example プラグインは forward プラグインより前に定義する必要がある。

example:github.com/coredns/example
forward:forward
$ git diff core/dnsserver/zdirectives.go core/plugin/zplugin.go
diff --git a/core/dnsserver/zdirectives.go b/core/dnsserver/zdirectives.go
index 56174955c..91ef82a7d 100644
--- a/core/dnsserver/zdirectives.go
+++ b/core/dnsserver/zdirectives.go
@@ -56,6 +56,7 @@ var Directives = []string{
        "secondary",
        "etcd",
        "loop",
+       "example",
        "forward",
        "grpc",
        "erratic",
diff --git a/core/plugin/zplugin.go b/core/plugin/zplugin.go
index 12bb4ce15..59bb64a51 100644
--- a/core/plugin/zplugin.go
+++ b/core/plugin/zplugin.go
@@ -57,4 +57,5 @@ import (
        _ "github.com/coredns/coredns/plugin/tsig"
        _ "github.com/coredns/coredns/plugin/view"
        _ "github.com/coredns/coredns/plugin/whoami"
+       _ "github.com/coredns/example"
 )

もし、プラグインが追加されていない場合は、CoreDNS の起動時に Corefile のパースに失敗する。

Corefile:6 - Error during parsing: Unknown directive 'example'

サンプルプラグイン

example プラグインでは、log.Info および log.Debug を使用しており、log.Debug の出力を確認するためには debug プラグインも有効にする必要がある。

.:10053 {
    forward . 1.1.1.1
    log
    errors
    debug
    example
}

example プラグインを有効にした状態で、上記の Corefile を使用することで、クエリの実行時に example プラグインが実行されログが出力されるようになる。

[DEBUG] plugin/example: Received response
[INFO] plugin/example: example
[INFO] 127.0.0.1:65349 - 13760 "A IN example.com. udp 40 false 4096" NOERROR qr,rd,ra,ad 67 0.013614538s

もし、debug プラグインが有効でない場合、log.Debug は出力されない。

[INFO] plugin/example: example
[INFO] 127.0.0.1:49451 - 57280 "A IN example.com. udp 40 false 4096" NOERROR qr,rd,ra,ad 67 0.008662355s

プラグインの実装

プラグインを実装する方法については、Writing Plugins および How to Add Plugins to CoreDNS に記載がある。
より具体的には、plugin.Handler interface を実装し、AddPluggin でプラグインを登録することで実装できる。
example プラグインがわかりやすいので参考にすると良い。

はじめに、setup を通して、plugin.Handler interface を実装している Example struct をプラグインとして追加している。

	// Add the Plugin to CoreDNS, so Servers can use it in their plugin chain.
	dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
		return Example{Next: next}
	})

ServeDNS では、プラグインが実行された際の処理を実装している。 また、plugin.NextOrFailure を使用することで、次のプラグインに処理を委譲することができる。
このため、plugin.NextOrFailure を意図的に呼びださないことで、プラグインチェーンの途中で処理を止めることができる。

func (e Example) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {

また、各プラグインでは、ログを出力するために、github.com/coredns/coredns/plugin/pkg/log を使用することができる。
このパッケージは、Go の log パッケージをラップしたもので、ログレベルのプレフィックスをつけたり、debug プラグインによって有効化されるデバッグログの管理も行なっている。

各プラグインの仕組み

例として、いくつかのプラグインの実装を見ていく。

cache

cache プラグインは、TTL をもとにクエリの結果をキャッシュすることができる。
どのようにキャッシュを行なっているかというと、plugin/cache/handler.godns.ResponseWriter のラッパーを作成し処理を移譲、その後のプラグインで WriteMsg() が呼ばれたらラッパーの WriteMsg() が呼ばれて、キャッシュするという仕組みである。

forward と cache プラグインを有効化した際の具体的な処理の流れとしては、下記のようになっている。

  1. plugin/cache/handler.go:42 - キャッシュが存在するか確認し、存在しなければ dns.ResponseWriter のラッパーを作成する
  2. plugin/cache/handler.go:108 - plugin.NextOrFailure を呼び出し、次のプラグインに処理を委譲する
  3. plugin/forward/forward.go:208 - forward プラグイン内でラッパーの WriteMsg() を呼び出す
  4. plugin/cache/cache.go:184 - ラッパーの WriteMsg() でキャッシュを行い、dns.ResponseWriterWriteMsg() を呼び出す

ログを確認するとわかるとおり、キャッシュ後はレスポンスが早くなっている。

[INFO] 127.0.0.1:64375 - 49069 "A IN example.com. udp 40 false 4096" NOERROR qr,rd,ra,ad 56 0.019756003s
[INFO] 127.0.0.1:50379 - 49563 "A IN example.com. udp 40 false 4096" NOERROR qr,aa,rd,ra,ad 56 0.000075345s

ready

ready プラグインは、/ready エンドポイントを有効にし、すべてのプラグインが使用可能なら OK、そうでなければ使用可能ではないプラグインを出力する。
どういう時に使うかというと、Kuberentes において CoreDNS がリクエストを受ける準備が完了したか確認するための readinessProbe を設定するために使用する。

$ curl -i localhost:8181/ready
HTTP/1.1 200 OK
Date: Wed, 01 Jan 2025 16:24:44 GMT
Content-Length: 2
Content-Type: text/plain; charset=utf-8

OK
$ curl -i localhost:8181/ready
HTTP/1.1 503 Service Unavailable
Date: Wed, 01 Jan 2025 16:24:16 GMT
Content-Length: 10
Content-Type: text/plain; charset=utf-8

errors,log

より具体的には、各プラグインで Readiness interface を実装することで、ready プラグインはそれを使用して、プラグインが使用可能かどうかを確認する。
なお、errors や log プラグインは、Readiness interface を実装していないので、上記の結果になることはない。

// The Readiness interface needs to be implemented by each plugin willing to provide a readiness check.
type Readiness interface {
	// Ready is called by ready to see whether the plugin is ready.
	Ready() bool
}

prometheus

prometheus プラグインは、prometheus および各プラグインが集計したメトリクスを prometheus 形式で出力するプラグインである。
メトリクスは、localhost:9153/metrics で公開されるようになっている。

$ curl -s localhost:9153/metrics | grep coredns_cache_hits_total
# HELP coredns_cache_hits_total The count of cache hits.
# TYPE coredns_cache_hits_total counter
coredns_cache_hits_total{server="dns://:10053",type="success",view="",zones="."} 9

実装としてはシンプルで、promhttp パッケージを使用して /metrics に対する HTTP ハンドラを登録、coredns_dns_requests_total などのメトリクスを集計している。
また、各プラグインでは、promauto パッケージを使用してメトリクスを定義しているので、このプラグインを有効化するだけでメトリクスを収集することができる。
たとえば、cache プラグインでは、plugin/cache/metrics.go にメトリクスを定義している。

	// cacheHits is counter of cache hits by cache type.
	cacheHits = promauto.NewCounterVec(prometheus.CounterOpts{
		Namespace: plugin.Namespace,
		Subsystem: "cache",
		Name:      "hits_total",
		Help:      "The count of cache hits.",
	}, []string{"server", "type", "zones", "view"})
© Ryota Sakamoto, 2025