PrometheusのサービスディスカバリをetcdとCoreDNSで実現する

# はじめに

この記事は[prometheus Advent Calendar 2017](https://qiita.com/advent-calendar/2017/prometheus)の21日目の記事です。23日に投稿していますが。。。

監視対象の追加はサービスティスカバリ(sd)によって、動的にするのが良いです。PrometheusにはAWS, Azure, GCP, OpenStack, Kubernetesなど多種多様なプラットフォームのメタデータを元にsdすることができます。が、所謂ベアメタルと呼ばれる、物理マシン上のプロセスの監視追加をsdによって実現しようとすると、諸々仕組みを整える必要があります。

という事で、以下の仕組みを検証/構築してみたので、その共有になります。

- CoreDNSのetcdバックエンドの機能によってetcdの情報をベースとしてDNSサービスを提供
- 監視対象はexporterの起動に合わせて、etcdに自身の監視エンドポイントを登録
- Prometheusのdns_sdを使って監視対象追加

# 全体像

全体像こんな感じです。

![Promtheus.png](https://qiita-image-store.s3.amazonaws.com/0/87808/66ee68b2-f1b7-9afc-b4e4-2a6b385e6f47.png)

# CoreDNS / etcdクラスタの用意

## etcdクラスタ

etcdクラスタの構築に関しては本筋では無いので、割愛します。
ぱっと見だと作るの難しそうなんですが、認証やTLS対応をしない場合は、結局リッスンするURLに関する設定やクラスタメンバの設定くらいなので、構築はそれほど難しくはないかと思います。

以下の記事が参考になります。
https://qiita.com/hana_shin/items/602f98bd9b153d22e50c

## CoreDNS

CoreDNSはetcdクラスタと同じホストに同居させるので各CoreDNSはローカルのetcdを見るようにします。

```:Corefile
prom.local {
    etcd prom.local {
        path /skydns
        endpoint http://localhost:2379
    }
    errors
    log
}
```

CoreDNSとはなんぞや？って方は手前味噌で恐縮ですが、以下の記事を参考にして頂ければ。
https://qiita.com/kanga/items/e74038f25d1f53ca6ade

coredns自体の起動は簡単です。

```
coredns -conf Corefile
```

とロードするCorefileを指定するだけで済みます。後はこれをSystemdなりなんなりでデーモン化すればOKです。

# DNS設定

etcdとCoreDNSの準備が整ったらDNSレコードが引けるようになります。CoreDNSが動いているサーバ上でdigが引ければOKです。

```
$ dig -p 53 @localhost SOA prom.local +noall +answer

; <<>> DiG 9.10.2-P4 <<>> -p 53 @localhost SOA prom.local +noall +answer
; (1 server found)
;; global options: +cmd
prom.local.	300	IN	SOA	ns.dns.prom.local. hostmaster.prom.local. 1513948062 7200 1800 86400 30
```

DNSに各種レコードを追加していきます。

## 親の内部DNSでサブドメインを委譲

内部DNSのサブドメイン`prom`をCoreDNSに委譲します。この辺の設定は使ってる内部DNSに依存するので割愛します。

## CoreDNSにNSレコードを登録

CoreDNSのetcdバックエンドはSOAレコードの向き先がデフォルトで`ns.dns`サブドメインに向いています。
`ns.dns.prom.local.`にnsレコードを設定していきます。

```shell-session

$ etcdctl set /skydns/local/prom/dns/ns/ns1 '{"host":"192.168.0.1"}'
{"host":"192.168.0.1"}
$ etcdctl set /skydns/local/prom/dns/ns/ns2 '{"host":"192.168.0.2"}'
{"host":"192.168.0.2"}
$ etcdctl set /skydns/local/prom/dns/ns/ns3 '{"host":"192.168.0.3"}'
{"host":"192.168.0.3"}
```
ちなみにCoreDNSのetcdバックエンドの機能は[SkyDNS](https://github.com/skynetservices/skydns)と完全に同じです。etcdのスラッシュ区切りのパスの逆順がDNSのFQDNに対応しています。

設定に問題無ければ、委譲されたサブドメインでAレコードとNSレコードが引けるようになります。

```
$ dig -p 53 @localhost  NS prom.local. +noall +answer

; <<>> DiG 9.10.2-P4 <<>> -p 53 @localhost NS prom.local. +noall +answer
; (1 server found)
;; global options: +cmd
prom.local.	300	IN	NS	ns1.ns.dns.prom.local.
prom.local.	300	IN	NS	ns2.ns.dns.prom.local.
prom.local.	300	IN	NS	ns3.ns.dns.prom.local.

$ dig -p 53 @localhost A prom.local. +noall +answer

; <<>> DiG 9.10.2-P4 <<>> -p 53 @localhost A prom.local. +noall +answer
; (1 server found)
;; global options: +cmd
prom.local.	300	IN	A	192.168.0.1
prom.local.	300	IN	A	192.168.0.2
prom.local.	300	IN	A	192.168.0.3
```
ここまで準備が終われば、内部dns経由で`prom.local`のレコードが引けるようになります。

## CoreDNSにetcdクライアント用のSRVレコードを登録

etcdにはDNSのSRVレコードから接続エンドポイントを指定する機能があります。SRVレコードはあまり馴染みの無い方も多いと思いますが、Aレコードの情報にポートと負荷分散の情報を加えたようなDNSのレコードのタイプになります。

```shell-session
$ etcdctl --discovery-srv prom.local ls
```

このコマンドを打つと、`_etcd-client-ssl._tcp.prom.local`あるいは`_etcd-client._tcp.prom.local`のドメインからSRVレコードを探して、名前解決できた先にetcdclientとして接続しに行きます。
今回はetcdをtls化していないので、`_etcd-client._tcp.prom.local`にレコードを追加していきます。etcdclientの接続先ポートの情報も付加します。

```shell-session
$ etcdctl set /skydns/local/prom/_tcp/_etcd-client/coreos-coredns01 '{"host":"192.168.0.1","port":2379}'
{"host":"192.168.0.1","port":2379}
$ etcdctl set /skydns/local/prom/_tcp/_etcd-client/coreos-coredns02 '{"host":"192.168.0.2","port":2379}'
{"host":"192.168.0.2","port":2379}
$ etcdctl set /skydns/local/prom/_tcp/_etcd-client/coreos-coredns03 '{"host":"192.168.0.3","port":2379}'
{"host":"192.168.0.3","port":2379}
```

SRVレコードが名前解決できるようになり、etcdctlからSRVレコード指定で接続が可能になります。

```
$ dig SRV _etcd-client._tcp.prom.local +noall +answer +additional

; <<>> DiG 9.10.2-P4 <<>> SRV _etcd-client._tcp.prom.local +noall +answer +additional
;; global options: +cmd
_etcd-client._tcp.prom.local. 300 IN SRV 10 33 2379 coreos-coredns02._etcd-client._tcp.prom.local.
_etcd-client._tcp.prom.local. 300 IN SRV 10 33 2379 coreos-coredns03._etcd-client._tcp.prom.local.
_etcd-client._tcp.prom.local. 300 IN SRV 10 33 2379 coreos-coredns01._etcd-client._tcp.prom.local.
coreos-coredns01._etcd-client._tcp.prom.local. 197 IN A	192.168.0.1
coreos-coredns02._etcd-client._tcp.prom.local. 300 IN A	192.168.0.2
coreos-coredns03._etcd-client._tcp.prom.local. 300 IN A	192.168.0.3
```

```shell-session
$ etcdctl --discovery-srv prom.local ls  
/skydns
```

これにより各etcdクライアントはetcdクラスタを構成する具体的なホスト、あるいはロードバランサのVIPを意識せずにetcdに接続できるようになりました。

# exporter側の設定

server01とserver02でnode_exporterを起動させて動的ディスカバリを実現させます。以下のようなSystemdのUnitファイルを用意します。

```bash:/etc/systemd/system/prom@.service
[Unit]
Description=promtheus node_exporter service

[Service]
Type=simple

Environment=EXPORTER_ADDRESS=192.168.1.1
Environment=EXPORTER_PORT=9100
Environment=ETCD_ENDPOINT=prom.local
Environment=ETCD_BASE_PATH=/skydns/local/prom
Environment=EXPORTER_TYPE=node

ExecStartPre=/usr/local/bin/register.sh set
ExecStart=/usr/bin/node_exporter
ExecStopPost=/usr/local/bin/register.sh rm

Restart=always
RestartSec=180s

[Install]
WantedBy=multi-user.target


```
ExecStartPreとExecStopPostでexprterを起動する前、停止した後にetcdに情報を登録するWrapスクリプトを呼んでいます。ラップスクリプトはこんな感じです。

```bash:/usr/local/bin/register.sh
#!/bin/bash

set -eu

subcommand="$1"
shift

case $subcommand in
    set)
        etcdctl --discovery-srv ${ETCD_ENDPOINT} set ${ETCD_BASE_PATH}/${EXPORTER_TYPE}/`hostname -s` \
            "{\"host\":\"${EXPORTER_ADDRESS}\",\"port\":${EXPORTER_PORT}}"
        ;;
    rm)
        etcdctl --discovery-srv ${ETCD_ENDPOINT} rm ${ETCD_BASE_PATH}/${EXPORTER_TYPE}/`hostname -s`
        ;;
    *)
        echo "Please specify set or rm as argument."
        ;;
esac

```

EXPORTER_ADDRESSなどの号機間で異なる値は、なんらか構成管理ツールで埋め込む想定です。あとはexporterを起動すると、etcdに情報が登録されDNSで名前解決できるようになります。

```shell-session
$ systemctl start prom@node.service
```

```
$ dig @localhost SRV node.prom.local +noall +answer            

; <<>> DiG 9.10.2-P4 <<>> @localhost SRV node.prom.local +noall +answer
; (1 server found)
;; global options: +cmd
node.prom.local. 300	IN	SRV	10 50 9100 server01.node.prom.local.
node.prom.local. 300	IN	SRV	10 50 9100 server02.node.prom.local.
```

# Prometheusの設定

後はPromtheusにsdの設定を入れるだけです。

```yaml:prometheus.yaml
global:
  scrape_interval: 15s
  evaluation_interval: 30s
  external_labels:
    monitor: 'global-monitor'
  
rule_files:
  - "/etc/prometheus/rule.d/*.rule"
    
scrape_configs:  
  - job_name: 'node_exporter'
    scrape_interval: 10s
    dns_sd_configs:
      - names:
        - "node.prom.local"
```

これでPrometheusを起動したら、先ほど起動したserver01とserver02が登録されています。

今回は`node_exporter`のみを監視対象に追加しましたが、`node.prom.local`や`mysql.prom.local`のように監視対象のミドル別に登録ドメインを変更する事も可能ですし、もっと手前でドメインを掘って`node.a.prom.local`や`node.b.prom.local`のようにA系B系C系...と用意して水平シャーディングする構成にもする事が可能です。

# おわりに

以上、ベアメタル環境での動的sdの一例でした。例ではsystemdを前提に進めましたがCentOS6など古い環境も混ざっている場合はUpstartでも同じような事ができると思います。

k8sで簡単にできる事を手間暇やって実現した感が否めないですが、ベアメタルでも監視をPrometheusに持っていくことで、よりk8sに移行しやすい環境を整える事ができるかな、と思います。

##### 反省点

検証してて気付いたのですが、DNSキャッシュの問題でPrometheusの反映が若干遅れるかも。。。その場合Prometheusは直接CoreDNSに名前を聞きにいったほうが良さそうだなぁ。。。