カスタムサービスディスカバリの実装

2018年7月5日筆者: Callum Styan

Prometheusには、Consul、Kubernetes、Azureなどのパブリッククラウドプロバイダーのような多くのサービスディスカバリ(SD)システムのための組み込み連携機能があります。しかし、世の中に存在するすべてのサービスディスカバリオプションの連携実装を提供することはできません。Prometheusチームは、現在のSD連携セットのサポートで手一杯であり、考えられるすべてのSDオプションの連携を維持するのは現実的ではありません。多くの場合、現在のSD実装はチーム外からの貢献であり、その後、適切にメンテナンスまたはテストされていません。私たちは、直接連携を提供できることがわかっており、意図したとおりに機能することがわかっているサービスディスカバリメカニズムとの直接連携にのみコミットすることを望んでいます。このため、現在、新しいSD連携についてはモラトリアム(一時停止)を実施しています。

しかし、Docker Swarmのような他のSDメカニズムとの連携を望む声があることは承知しています。最近、Prometheusリポジトリのドキュメントのディレクトリに、Prometheusのメインバイナリにマージすることなくカスタムサービスディスカバリ連携を実装するための小さなコード変更と例がコミットされました。このコード変更により、内部のDiscovery Managerコードを利用して、新しいSDメカニズムとやり取りし、Prometheusのfile_sdと互換性のあるファイルを出力する別の実行可能ファイルを作成できます。Prometheusとこの新しい実行可能ファイルを共置することで、Prometheusにこの実行可能ファイルからのfile_sd互換の出力を読み込ませ、それによってそのサービスディスカバリメカニズムからターゲットをスクレイピングするように設定できます。将来的には、これによりメインのPrometheusバイナリからSD連携を移動させることができるようになります。また、アダプターを利用する安定したSD連携をPrometheusのdiscoveryパッケージに移動させることも可能になります。

アダプターコードで実装されているfile_sdを使用した連携は、こちらにリストされています。

例のコードを見てみましょう。

アダプター

まず、adapter.goというファイルがあります。カスタムSD実装のためにこのファイルをコピーするだけで十分ですが、何が起こっているのかを理解しておくと役立ちます。

// Adapter runs an unknown service discovery implementation and converts its target groups
// to JSON and writes to a file for file_sd.
type Adapter struct {
    ctx     context.Context
    disc    discovery.Discoverer
    groups  map[string]*customSD
    manager *discovery.Manager
    output  string
    name    string
    logger  log.Logger
}

// Run starts a Discovery Manager and the custom service discovery implementation.
func (a *Adapter) Run() {
    go a.manager.Run()
    a.manager.StartCustomProvider(a.ctx, a.name, a.disc)
    go a.runCustomSD(a.ctx)
}

アダプターはdiscovery.Managerを利用して、カスタムSDプロバイダーのRun関数をgoroutineで実行します。Managerは、カスタムSDが更新を送信するチャンネルを持っています。これらの更新にはSDターゲットが含まれます。groupsフィールドには、カスタムSD実行可能ファイルがSDメカニズムから認識しているすべてのターゲットとラベルが含まれます。

type customSD struct {
    Targets []string          `json:"targets"`
    Labels  map[string]string `json:"labels"`
}

このcustomSD構造体は、主にPrometheusの内部targetgroup.Group構造体をfile_sd形式のJSONに変換するために存在します。

実行中、アダプターはカスタムSD実装からの更新をチャンネルでリッスンします。更新を受信すると、targetgroup.Groupsを別のmap[string]*customSDに解析し、Adapterのgroupsフィールドに格納されているものと比較します。両者が異なる場合、新しいgroupsをAdapter構造体に割り当て、JSONとして出力ファイルに書き込みます。この実装は、チャンネルを介してSD実装から送信される各更新が、SDが認識しているすべてのターゲットグループの完全なリストを含むことを前提としています。

カスタムSD実装

次に、実際のアダプターを使用して独自のカスタムSDを実装します。完全に機能する例は、同じexamplesディレクトリにあるこちらで確認できます。

ここでは、アダプターコード"github.com/prometheus/prometheus/documentation/examples/custom-sd/adapter"とその他のPrometheusライブラリをインポートしていることがわかります。カスタムSDを作成するには、Discovererインターフェースの実装が必要です。

// Discoverer provides information about target groups. It maintains a set
// of sources from which TargetGroups can originate. Whenever a discovery provider
// detects a potential change, it sends the TargetGroup through its channel.
//
// Discoverer does not know if an actual change happened.
// It does guarantee that it sends the new TargetGroup whenever a change happens.
//
// Discoverers should initially send a full set of all discoverable TargetGroups.
type Discoverer interface {
    // Run hands a channel to the discovery provider(consul,dns etc) through which it can send
    // updated target groups.
    // Must returns if the context gets canceled. It should not close the update
    // channel on returning.
    Run(ctx context.Context, up chan<- []*targetgroup.Group)
}

実装する必要があるのは、Run(ctx context.Context, up chan<- []*targetgroup.Group)という1つの関数だけです。これは、Adapterコード内のマネージャーがgoroutine内で呼び出す関数です。Run関数は、コンテキストを使用して終了時期を知り、ターゲットグループの更新を送信するためのチャンネルが渡されます。

提供されている例のRun関数を見ると、他のSDの実装で私たちが行う必要があるいくつかの重要なことがわかります。ここでは(組み込みのConsul SD実装がすでに存在しないという仮定で)Consulに定期的に呼び出しを行い、応答をtargetgroup.Group構造体のセットに変換します。Consulの仕組みのため、まず既知のすべてのサービスを取得する呼び出しを行い、次に各サービスごとにバックエンドインスタンスに関する情報を取得する別の呼び出しを行う必要があります。

各サービスに対してConsulに呼び出しを行うループの上にあるコメントに注意してください。

// Note that we treat errors when querying specific consul services as fatal for for this
// iteration of the time.Tick loop. It's better to have some stale targets than an incomplete
// list of targets simply because there may have been a timeout. If the service is actually
// gone as far as consul is concerned, that will be picked up during the next iteration of
// the outer loop.

これにより、すべてのターゲットの情報が取得できない場合は、不完全な更新を送信するよりも、まったく更新を送信しない方が良いということになります。一時的なネットワークの問題、プロセスの再起動、HTTPタイムアウトによる誤検知を防ぐために、短い期間は古いターゲットのリストを持つことを優先します。Consulからすべてのターゲットに関する応答があった場合は、それらのターゲットすべてをチャンネルに送信します。また、parseServiceNodesというヘルパー関数があり、個々のサービスに対するConsulの応答を受け取り、バックエンドノードからラベル付きのターゲットグループを作成します。

現在の例を使用する

独自のカスタムSD実装を記述し始める前に、コードを確認した後、現在の例を実行するのが良いでしょう。簡単にするために、例のコードを扱う際は、通常、docker-composeを使用してConsulとPrometheusの両方をDockerコンテナとして実行します。

docker-compose.yml

version: '2'
services:
consul:
    image: consul:latest
    container_name: consul
    ports:
    - 8300:8300
    - 8500:8500
    volumes:
    - ${PWD}/consul.json:/consul/config/consul.json
prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    volumes:
    - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
    - 9090:9090

consul.json

{
"service": {
    "name": "prometheus",
    "port": 9090,
    "checks": [
    {
        "id": "metrics",
        "name": "Prometheus Server Metrics",
        "http": "http://prometheus:9090/metrics",
        "interval": "10s"
    }
    ]

}
}

docker-composeで両方のコンテナを起動し、例のmain.goを実行すると、localhost:8500でConsul HTTP APIに問い合わせが行われ、file_sd互換のファイルがcustom_sd.jsonとして書き込まれます。Prometheusにfile_sd設定でこのファイルを取得するように設定できます。

scrape_configs:
  - job_name: "custom-sd"
    scrape_interval: "15s"
    file_sd_configs:
    - files:
      - /path/to/custom_sd.json