tkuchikiの日記

新ブログ https://blog.tkuchiki.net

Consul Multiple Datacenters 検証結果 +α

この記事は、HashiCorp Advent Calendar 2015 17日目の記事です。

検証環境の Consul は、

$ consul --version
Consul v0.6.0
Consul Protocol: 3 (Understands back to: 1)

です。
検証環境は v0.6.0 ですが、v0.6.0 に依存した話しはほとんどないと思います。

筆者は、Consul を Multiple Datacenters(以降、Multi-DC) で運用しております。
Multi-DC で Consul を導入する際に検証した結果を記します。
トピックは以下のとおりです。

  • WAN
  • DC をまたいで DNS を引く
  • DC をまたいで KV のデータを取得する
  • DC をまたいで KV にデータを入れる
  • Consul Event は複数の DC に一斉に送れない
  • DNS forward に Unbound を使う
  • External Services

準備

Consul のバイナリを入手します。

$ curl -LO https://releases.hashicorp.com/consul/0.6.0/consul_0.6.0_linux_amd64.zip
$ unzip consul_0.6.0_linux_amd64.zip

実験のために何台もサーバを立てるのは大変なので、
検証用に 1 台のサーバで consul を複数起動する - tkuchikiの日記 のような手順で、
1 台のサーバに Server x 3, Client x 1 のクラスタを 2セット作ります。

dc01

$ ./consul members
Node           Address         Status  Type    Build  Protocol  DC
consul01-dc01  10.0.2.15:8301  alive   server  0.6.0  2         dc01
consul02-dc01  10.0.2.15:8311  alive   server  0.6.0  2         dc01
consul03-dc01  10.0.2.15:8321  alive   server  0.6.0  2         dc01
consul04-dc01  10.0.2.15:8331  alive   client  0.6.0  2         dc01
$ curl http://127.0.0.1:8500/v1/status/leader
"10.0.2.15:8300"

dc02

$ ./consul members -rpc-addr=127.0.0.1:18400
Node           Address          Status  Type    Build  Protocol  DC
consul01-dc02  10.0.2.15:18301  alive   server  0.6.0  2         dc02
consul02-dc02  10.0.2.15:18311  alive   server  0.6.0  2         dc02
consul03-dc02  10.0.2.15:18321  alive   server  0.6.0  2         dc02
consul04-dc02  10.0.2.15:18331  alive   client  0.6.0  2         dc02
$ curl http://127.0.0.1:18500/v1/status/leader
"10.0.2.15:18300"

WAN

WAN は、すべての DC を含めた Consul クラスタ全体のことです。
consul members -wan は Multi-DC の Consul Server のみを返します。

$ ./consul members -wan
Node                Address          Status  Type    Build  Protocol  DC
consul01-dc01.dc01  10.0.2.15:8302   alive   server  0.6.0  2         dc01
consul01-dc02.dc02  10.0.2.15:18302  alive   server  0.6.0  2         dc02
consul02-dc01.dc01  10.0.2.15:8312   alive   server  0.6.0  2         dc01
consul02-dc02.dc02  10.0.2.15:18312  alive   server  0.6.0  2         dc02
consul03-dc01.dc01  10.0.2.15:8322   alive   server  0.6.0  2         dc01
consul03-dc02.dc02  10.0.2.15:18322  alive   server  0.6.0  2         dc02

client に向けて consul members -wan を実行すると、
何も出力せず、status code 2 を返します。

$ ./consul members -rpc-addr=127.0.0.1:8430 -wan
$ echo $?
2

DC をまたいで DNS を引く

dc01 の Consul に対して、dc02 の node を問い合わせます。

$ dig @127.0.0.1 -p 8630 consul04-dc02.node.dc02.consul A

; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.37.rc1.el6_7.2 <<>> @127.0.0.1 -p 8630 consul04-dc02.node.dc02.consul A
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 19552
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;consul04-dc02.node.dc02.consul.        IN      A

;; ANSWER SECTION:
consul04-dc02.node.dc02.consul. 60 IN   A       10.0.2.15

;; Query time: 1 msec
;; SERVER: 127.0.0.1#8630(127.0.0.1)
;; WHEN: Tue Dec 15 17:58:09 2015
;; MSG SIZE  rcvd: 94

A レコードが返ってきます。
DNS Interface - Consul by HashiCorp に書いてありますが、

<node>.node[.datacenter].<domain> なので、
同一 DC にいる場合は、 [.datacenter] を省略することができます。

$ dig @127.0.0.1 -p 8630 consul04-dc01.node.consul A

; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.37.rc1.el6_7.2 <<>> @127.0.0.1 -p 8630 consul04-dc01.node.consul A
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 46271
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;consul04-dc01.node.consul.     IN      A

;; ANSWER SECTION:
consul04-dc01.node.consul. 60   IN      A       10.0.2.15

;; Query time: 0 msec
;; SERVER: 127.0.0.1#8630(127.0.0.1)
;; WHEN: Tue Dec 15 18:05:00 2015
;; MSG SIZE  rcvd: 84

DC をまたいで KV のデータを取得する

まず、通常の KV にデータを PUT して取り出す場合です。
dc01 に PUT します。

$ curl -X PUT -d "dc01-test" http://127.0.0.1:8500/v1/kv/dc01
true
$ curl http://127.0.0.1:8500/v1/kv/dc01?raw
dc01-test
$ curl http://127.0.0.1:8510/v1/kv/dc01?raw
dc01-test
$ curl http://127.0.0.1:8520/v1/kv/dc01?raw
dc01-test
$ curl http://127.0.0.1:8530/v1/kv/dc01?raw
dc01-test

dc01 内で KV のデータが共有されていますね。
この状態で、dc02 の KV を参照すると、

$ curl http://127.0.0.1:18500/v1/kv/dc01?raw

データが入っていませんね。
これは、仕様なのでおかしい挙動ではありません。

追記: 2016-08-23 19:30

KV をまたいでデータを取得するためには、以下のように dc= をつければ OK です。訂正致します。

curl -s 'data' http://127.0.0.1:18500/v1/kv/data?dc=dc02&raw"

ちなみに、consul-replicate というものがあるので、
これを使えば KV のデータをレプリケーションすることができます。
しかし、

  • consul-replicate をどのサーバで動かすか
  • consul-replicate の HA構成はどうするか
  • consul-replicate のレプリケーションが一方向なので、DC 間での同期はどうするのか

といった問題があるので、使いどころが難しそうです。

DC をまたいで KV にデータを入れる

追記: 2016-08-23 19:30

curl -s -X PUT -d 'data' http://127.0.0.1:18500/v1/kv/data?dc=dc02"

のように、dc= をつければ datacenter をまたいで KV にデータをいれることができました。
間違ってた情報を書いていたので、そちらは削除します。

Consul Event は複数の DC に一斉に送れない

consul event には、-datacenter というオプションがありますが、
指定できる DC は一つだけです。
複数の DC に Event を送りたい場合は、DC ごとに実行するようです。

Unbound で DNS forward する

Dnsmasq で Consul DNS に forward して内部 DNS に使っている方が多いと思いますが、
Unbound でも forward できます。

# unbound.conf

server:
    access-control: 127.0.0.0/8 allow
    do-not-query-localhost: no

forward-zone:
    name: "consul"
    forward-addr: 127.0.0.1@8600

Unbound の設定に明るくなくて、
デフォルトの設定に上記設定を追加したらうまく forward できませんでした...
設定が最低限すぎるので、適宜設定を追加してお使いください。

External Services

Consul Service は、Consul クラスタ内のサービスを監視しますが、
External Services を使うと、外部のサービスを監視することができます。

$ curl -X PUT -d '{"Datacenter": "dc01", "Node": "google", "Address": "www.google.com", "Service": {"Service": "search", "Port": 80}}' http://127.0.0.1:8500/v1/catalog/register
true

External Services を使うためには、recursor または recursorsDNS サーバを指定しなくてはなりません。
consul01-dc01(port 8600) は recursor を設定せず、
consul02-dc01(port 8610) は設定しているとします。
その場合、以下の様な実行結果になります。

$ dig @127.0.0.1 -p 8600 search.service.consul

; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.37.rc1.el6_7.2 <<>> @127.0.0.1 -p 8600 search.service.consul
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 56306
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;search.service.consul.         IN      A

;; ANSWER SECTION:
search.service.consul.  15      IN      CNAME   www.google.com.

;; Query time: 0 msec
;; SERVER: 127.0.0.1#8600(127.0.0.1)
;; WHEN: Tue Dec 15 19:41:34 2015
;; MSG SIZE  rcvd: 88
$ dig @127.0.0.1 -p 8610 search.service.consul

; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.37.rc1.el6_7.2 <<>> @127.0.0.1 -p 8610 search.service.consul
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26004
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;search.service.consul.         IN      A

;; ANSWER SECTION:
search.service.consul.  15      IN      CNAME   www.google.com.
www.google.com.         149     IN      A       216.58.220.196

;; Query time: 11 msec
;; SERVER: 127.0.0.1#8610(127.0.0.1)
;; WHEN: Tue Dec 15 19:42:01 2015
;; MSG SIZE  rcvd: 118

recursor を設定した consul02-dc01 は、A レコードが返ってきます。
recursor の反映は consul reload ではできませんので、再起動する必要があります。

※追記
recursor を設定していない状態からの反映は consul reload ではできませんが、
recursor を設定している状態からは consul reload で変更を反映することができます。

まとめ

Consul の Multi-DC について検証した結果 +α を紹介しました。
使った設定ファイルは https://gist.github.com/tkuchiki/c4ea89df94c8e2a51b83 に置いてあります。

明日は、Multiple DNS recursors の対応ありがとうございます!(http://fstn.hateblo.jp/entry/2015/02/21/024400)
な、@foostan さんです!

mackerel-plugin-rack-stats を例にした mackerel agent plugin の作り方

この記事は、Mackerel Advent Calendar 2015 16日目の記事です。

mackerel-agent-plugins には様々な plugin がありますが、無い場合は自作する必要がありますよね。
ということで、mackerel-plugin-rack-stats を例に mackerel agent plugin の作り方を紹介します。

mackerel-plugin-rack-stats

raindrops の stats を収集する plugin です。
raindrops が Linux でしか動かないようですので、
必然的にこのプラグインLinux でしか利用できません。

mackerel agent plugin の作り方

go-mackerel-plugin-helper を使うと簡単に実装することができます。
mackerel-agent-plugins の中には go-mackerel-plugin を使っているものもありますが、
1年近く更新されていないようですので、go-mackerel-plugin-helper を使うのが良さそうです。

それでは、順に説明していきます。

func main()

// mackerel-plugin-rack-stats
package main

import (
    "flag"
    "fmt"
    ...
    mp "github.com/mackerelio/go-mackerel-plugin-helper"
)

func main() {
    optAddress := flag.String("address", "http://localhost:8080", "URL or Unix Domain Socket")
    optPath := flag.String("path", "/_raindrops", "Path")
    optMetricKey := flag.String("metric-key", "", "Metric Key")
    optVersion := flag.Bool("version", false, "Version")
    optTempfile := flag.String("tempfile", "", "Temp file name")
    flag.Parse()

    if *optVersion {
        fmt.Println("0.2")
        os.Exit(0)
    }

    var rack RackStatsPlugin
    rack.Address = *optAddress
    rack.Path = *optPath
    rack.MetricKey = *optMetricKey
    helper := mp.NewMackerelPlugin(rack)

    if *optTempfile != "" {
        helper.Tempfile = *optTempfile
    } else {
        helper.Tempfile = fmt.Sprintf("/tmp/mackerel-plugin-rack-stats")
    }

    helper.Run()
}

オプション解析に標準モジュールの flag を使っていますが、
機能が充実したライブラリを使用しても良いと思います。
以降は、処理ごとに分割して説明します。

MackerelPlugin の生成

// mackerel-plugin-rack-stats
var rack RackStatsPlugin
rack.Address = *optAddress
rack.Path = *optPath
rack.MetricKey = *optMetricKey
helper := mp.NewMackerelPlugin(rack)

go-mackerel-plugin-helper.NewMackerelPluginMackerelPlugin 構造体を生成します。
MackerelPluginPlugin を変数に持っており、
PluginFetchMetrics()GraphDefinition() を実装する必要があります。
それぞれの定義は以下のとおりです。

// go-mackerel-plugin-helper
type Plugin interface {
    FetchMetrics() (map[string]interface{}, error)
    GraphDefinition() map[string]Graphs
}

type MackerelPlugin struct {
    Plugin
    Tempfile string
}

func NewMackerelPlugin(plugin Plugin) MackerelPlugin {
    mp := MackerelPlugin{plugin, "/tmp/mackerel-plugin-default"}
    return mp
}

FetchMetrics()GraphDefinition() の説明は後ほど行います。

GraphDefinition() の返り値である Graphs と、
その引数である Metrics の定義は以下のとおりです。

// go-mackerel-plugin-helper

type Graphs struct {
    Label   string    `json:"label"`
    Unit    string    `json:"unit"`
    Metrics []Metrics `json:"metrics"`
}

type Metrics struct {
    Name    string  `json:"name"`
    Label   string  `json:"label"`
    Diff    bool    `json:"diff"`
    Type    string  `json:"type"`
    Stacked bool    `json:"stacked"`
    Scale   float64 `json:"scale"`
}

GraphsMetrics の詳細にも触れておきましょう。

Graphs

  • Label

f:id:tkuchiki:20151213152248p:plain

赤枠で囲った memory の部分の設定です。

  • Unit

float, integer, percentage, bytes, bytes/sec, iops のいずれかを指定します。

Metrics

  • Name

カスタムメトリック名です。
ワイルドカードとして、*, # を使うことができます。

  • Label

f:id:tkuchiki:20151213152408p:plain

赤枠で囲った used, buffers などの設定です。
API仕様(v0)/api/v0/graph-defs/create に書いてありますが、

%1、%2.. のようにしてメトリック名の1つ目、2つ目.. のワイルドカードにマッチした部分を利用することができます。

とあるように、%N を指定することもできます。

  • Diff

グラフを差分値で出力する設定です(デフォルト false)。

  • Type

データの型です。
float64, uint32, uint64 のいずれかを指定します(デフォルト float64)。

  • Stacked

積み重ねグラフにする設定です(デフォルト false = 線グラフ)。

  • Scale

文字通り目盛りで、Mackerel に送る値を調整してくれます。
例えば、free -k の実行結果を parse して、Mackerel に送る plugin があったとします。
free -k の実行結果は単位が KB なので、Bytes にする場合 1024 を乗算する必要があります。
Scale を 1024 とした場合、1024 を乗算して Mackerel にデータを送ってくれます。

Tempfile

if *optTempfile != "" {
    helper.Tempfile = *optTempfile
} else {
    helper.Tempfile = fmt.Sprintf("/tmp/mackerel-plugin-rack-stats")
}

差分値の出力に使います。
Tempfile は差分値の出力以外では使っていなさそうでした。
mp := MackerelPlugin{plugin, "/tmp/mackerel-plugin-default"} となっているので、
Tempfileの設定をしないと/tmp/mackerel-plugin-defaultに実行結果のJSONが出力されます。

plugin 実行

helper.Run()

でグラフの設定とメトリック収集処理を実行します。
他のプラグインを見ると、ほとんどが以下のような実装になっています。

if os.Getenv("MACKEREL_AGENT_PLUGIN_META") != "" {
    helper.OutputDefinitions()
} else {
    helper.OutputValues()
}

しかし、MackerelPlugin.Run() は内部で同じことを行っているため、
Run() を使うのが良いでしょう。
なぜ、2015-12-16 現在、ほとんどのプラグインで使われていないかのかというと、
Run() が実装されたのが da2aa407cda88bca4a1159f32bfdcee078ad20fc のコミットで、
2015-12-03 に実装されたばかりだからですね。

mackerel-agent が、環境変数 MACKEREL_AGENT_PLUGIN_META をつけて実行した場合は、
OutputDefinitions() が実行され、グラフ定義の json が出力されます。
この時、GraphDefinition() が実行されます。
環境変数がついていない場合は、OutputValues() が実行され、
メトリックが出力されます。
こちらは、FetchMetrics()GraphDefinition() の両方が実行されます。

FetchMetrics() (map[string]interface{}, error)

メトリックの収集処理です。
mackerel-plugin-rack-stats では、 GET /_raindrops のレスポンスを parse して map[string]interface{} を生成します。
細かい処理は省略しますが、map[string]interface{} は以下のようになっています。

map[string]interface{
    "active": N.nnnnnn,
    "queued": N.nnnnnn,
    "calling": N.nnnnnn,
    "writing": N.nnnnnn,
}

このとき、map[string]interface{} のキーが、
GraphDefinition()map[string](mp.Graphs)Metrics.Name
対応している必要があります(この例では、active, queued, calling, writing)。

GraphDefinition() map[string]Graphs

func (u RackStatsPlugin) GraphDefinition() map[string](mp.Graphs) {
        scheme, path, err := parseAddress(u.Address)
        if err != nil {
                log.Fatal(err)
        }

        var label string
        if u.MetricKey == "" {
                switch scheme {
                case "http":
                        _, port, _ := net.SplitHostPort(path)
                        u.MetricKey = port
                        label = fmt.Sprintf("Rack Port %s Stats", port)
                case "unix":
                        u.MetricKey = strings.Replace(strings.Replace(path, "/", "_", -1), ".", "_", -1)
                        label = fmt.Sprintf("Rack %s Stats", path)
                }
        } else {
                label = fmt.Sprintf("Rack %s Stats", u.MetricKey)
        }

        return map[string](mp.Graphs){
                fmt.Sprintf("%s.rack.stats", u.MetricKey): mp.Graphs{
                        Label: label,
                        Unit:  "integer",
                        Metrics: [](mp.Metrics){
                                mp.Metrics{Name: "queued", Label: "Queued", Diff: false},
                                mp.Metrics{Name: "active", Label: "Active", Diff: false},
                                mp.Metrics{Name: "writing", Label: "Writing", Diff: false},
                                mp.Metrics{Name: "calling", Label: "Calling", Diff: false},
                        },
                },
        }
}

1 ホストに複数設定できるように、map[string](mp.Graphs) のキーを変更できるようになっているため処理が複雑になっていますが、
スルーして return している map[string](mp.Graphs) に注目してください。
わかりやすくするために、fmt.Sprintf で生成しているキーや label を展開した状態の map[string](mp.Graphs) を以下に示します。

map[string](mp.Graphs){
    "8080.rack.stats": mp.Graphs{
        Label: "Rack Port 8080 Stats",
        Unit:  "integer",
        Metrics: [](mp.Metrics){
            mp.Metrics{Name: "active", Label: "Active", Diff: false},
            mp.Metrics{Name: "queued", Label: "Queued", Diff: false},
            mp.Metrics{Name: "calling", Label: "Calling", Diff: false},
            mp.Metrics{Name: "writing", Label: "Writing", Diff: false},
        },
    },
}

前述のとおり、Metrics.Name の値が、
FetchMetrics() が返す map[string]interface{} のキーと
対応している必要があります(この例では、active, queued, calling, writing)。

Metrics.Nameワイルドカードを使う場合

Metrics.Nameワイルドカードとして、*# を使うことができます。
ワイルドカードを使う場合、GraphDefinition()map[string](mp.Graphs) のキーと
FetchMetrics() が返す map[string]interface{}のキーを
連結したものにしなくてはなりません。
map[string](mp.Graphs) のキーが 8080.rack.stats だった場合、
map[string]interface{}

// FetchMetrics()
map[string]interface{
    "8080.rack.stats.active": N.nnnnnn,
    "8080.rack.stats.queued": N.nnnnnn,
    "8080.rack.stats.calling": N.nnnnnn,
    "8080.rack.stats.writing": N.nnnnnn,
}

となります。
ワイルドカードを使うと、GraphDefinition()map[string](mp.Graphs)
以下のように書き換えることができます。

// GraphDefinition()
return map[string](mp.Graphs){
    "8080.rack.stats" : mp.Graphs{
        Label: "Rack Port 8080 Stats",
        Unit:  "integer",
        Metrics: [](mp.Metrics){
            mp.Metrics{Name: "*", Label: "%1", Diff: false},
        },
    },
}

前述したとおり、ワイルドカードにマッチした部分を Label でつかうことができます。

map[string](mp.Graphs) のキー にワイルドカードを使う場合

ワイルドカードは、map[string](mp.Graphs) のキーにも使うことができます。
こちらは、グラフの凡例をグループ化する場合に使います。
mackerel-plugin-rack-stats には当てはまりませんが、
FetchMetrics() が以下のようなデータを返すとし、

// FetchMetrics()
map[string]interface{
    "rack.foo.stats.active": 10.000000,
    "rack.foo.stats.queued": 10.000000,
    "rack.foo.stats.calling": 10.000000,
    "rack.foo.stats.writing": 10.000000,
    "rack.hoge.stats.active": 5.000000,
    "rack.hoge.stats.queued": 5.000000,
    "rack.hoge.stats.calling": 5.000000,
    "rack.hoge.stats.writing": 5.000000,
}

GraphDefinition()map[string](mp.Graphs) が以下のような実装になっているとします。

// GraphDefinition()
map[string](mp.Graphs){
    "rack.#.stats": mp.Graphs{
        Label: "Rack test Stats",
        Unit:  "integer",
        Metrics: [](mp.Metrics){
            mp.Metrics{Name: "active", Label: "Active", Diff: false},
            mp.Metrics{Name: "queued", Label: "Queued", Diff: false},
            mp.Metrics{Name: "calling", Label: "Calling", Diff: false},
            mp.Metrics{Name: "writing", Label: "Writing", Diff: false},
        },
    },
}

その場合、グラフは

f:id:tkuchiki:20151213210005p:plain

となります。
foohoge でグループ化されていますね。

API仕様にも書いてありますが、

またグラフ定義のメトリック名にはワイルドカード(* または #)を 2つのドット(.)の間、または最後のドット(.)の後ろに単独で使用することができます。

とあるので、#.rack.stats のようには定義できないようです。
ちなみに、#* の使い分けについては、

ワイルドカード # を使った場合はメトリック名の# にマッチした部分でグラフの凡例がグループ化されます。

とありますし、色々なプラグインを見たところ、map[string](mp.Graphs) のキーは #Metrics.Name では * が使われていましたので、慣例に従うと良いと思います。

またワイルドカード # は一つまでしか使えません。メトリック名全体は ^custom(.[-a-zA-Z0-9_]+|[*#])+ のようになります。

という制約もありますのでご注意ください。

以上が mackerel agent plugin の作り方でした。
長々と説明しましたが、

  • 自作の Plugin 構造体
  • Plugin 構造体に FetchMetrics() (map[string]interface{}, error)
  • Plugin 構造体に GraphDefinition() map[string]Graphs

を実装するだけですのでお手軽ですね!

mackerel-plugin-rack-stats の使い方

mackerel-plugin-rack-stats の説明があまりできていなかったので軽く紹介します。

-address で指定した Port または Unix Domain Socket に、
-path で指定した(デフォルト /_raindrops) に GET リクエストを送ります。
1ホストに Rack server が複数起動している場合にも対応できるように、
-metric-key を指定すると、別のメトリックとして登録することができます。
-metric-key を指定しない場合は、
PORT.rack.stats_path_to_unicorn_sock.rack.stats のようなメトリック名になります。 config の例は以下のとおりです。

[plugin.metrics.rack_stats]
command = "/path/to/mackerel-plugin-rack-stats -address=unix:/path/to/unicorn.sock"
[plugin.metrics.rack_stats]
command = "/path/to/mackerel-plugin-rack-stats -address=http://localhost:8080"

他にも Rack の stats を収集する gem がありましたら教えて下さい...

まとめ

mackerel-plugin-rack-stats を例に mackerel agent plugin の作り方を紹介しました。
go-mackerel-plugin-helper を使うと簡単ですので、皆さんも実装してみてください!

17日目の担当は、@shiozaki さんです!

Mackerel のホスト名を ssh の補完候補リストに出力する

この記事は、Mackerel Advent Calendar 2015 8日目の記事です。

Mackerel に登録しているホストに ssh するとき、
補完できたら楽かもしれないと思い、その実現方法を模索してみました。
補完機能を一から書くのは大変なので、
bash-completion を使います。
動作環境は Linux です。

※紹介する方法は、ローカルマシンからの ssh で補完する方法ではなく、
リモートマシンからリモートマシンへ ssh する際に補完する方法です。

bash-completion

bash-completion は、bash の補完機能を拡張するものです。
細かく説明されている記事がいくつもあると思いますので説明は省きます。
以下のようにインストールできます。

yum

yum install -y epel-release
yum install -y --enablerepo=epel bash-completion

apt-get

apt-get install -y bash-completion

補完

consul membersの結果でbash補完する - Qiita を参考に、
_known_hosts_real() を上書きして補完できるようにします。
consul の場合は DNS の機能があるので、
dnsmasq などと組み合わせることで、ホスト名で名前解決できます。
そのため、consul members でホスト名一覧を取ることができれば、
ssh に使うことができます。
しかし、今回のケースでは、ホストの一覧を取るだけでは、
ssh することができません。
bash-completion の補完は、
/etc/hosts, ~/.ssh/config, ~/.ssh/known_hosts を使うようですので、
/etc/hosts, ~/.ssh/config に自動で設定を追加することで、
補完できるようにする方法を考えます。
Mackerel の /api/v0/hosts.json を parse するのに jq を使いますので、
yum, apt-get もしくは source build してインストールしてください。

/etc/hosts を使う

_known_hosts_real() を使うと書いておきながら、
/etc/hosts に設定を追加する場合、
root 以外は書き込めないので使えなさそうです。
仕方がないので cron で定期的に更新します。
以下のようなスクリプトになりました。

ssh で入れるホストには mackerel-agent が入っていると思いますので、
mackerel-agent.conf から apikey を取得して使っています。
curlhttps に対して Request を送ると dentry_cache が肥大化してしまうので、
export NSS_SDB_USE_CACHE=yes することをおすすめします。
cron の場合、普通に使えば毎分より短い間隔では実行できないので、
Mackerel のサーバへの負荷もそれほど高くないと思いますが、
台数に応じて実行間隔をばらつかせる、実行間隔を伸ばすなどしましょう。

実行例

Mackerel に、

  • server01 192.168.1.100
  • server02 192.168.1.110
  • server03 192.168.1.120
  • server04 192.168.1.130

というホストが登録されていると仮定します。

$ ssh s[Tab]
$ ssh server0[Tab]
server01  server02    server03  server04

といった具合に補完されます。

/etc/hosts には以下のように設定が追記されます。

192.168.1.100 server01 # mackerel
192.168.1.110 server02 # mackerel
192.168.1.120 server03 # mackerel
192.168.1.130 server04 # mackerel

# mackerel を検索して追記に利用していますので、
事前に /etc/hosts に色々な設定が書いてあっても、
影響することはありません。
また、追記する前に古い設定を削除していますので、
同じような設定が無限に追記されていくことはありません。

~/.ssh/config を使う

/etc/hosts の場合、root 以外は書き込めないので
_known_hosts_real() を使えませんでしたが、
~/.ssh/config であれば、自分の権限で書き込むことができます。
/etc/bash_completion_known_hosts_real() を、
以下のコードで上書きすると ~/.ssh/config に動的に設定を追加することができます。

ファイルの更新間隔をチェックして、
interval で設定した秒数経過するまで api を叩かないようにしています。
こちらも同時に使用するユーザ数に応じてばらつかせたり、
間隔を伸ばしたりしましょう。

実行例

補完のされ方は /etc/hosts のときと同じです。

~/.ssh/config には以下のように追記されます。

Host server01 # mackerel
Hostname 192.168.1.100
Host server02 # mackerel
Hostname 192.168.1.110
Host server03 # mackerel
Hostname 192.168.1.120
Host server04 # mackerel
Hostname 192.168.1.130

こちらも # mackerel を検索して追記に利用していますので、
~/.ssh/config に色々設定していた場合でも影響はありませんし、
設定が無限に追記されていくこともありません。

まとめ

ssh する際に Mackerel のホストを補完する方法を紹介しました。
かなり無理やりな方法ですので、もっと良い方法をご存知でしたら教えて下さい...
また、ローカルホストから ssh するときにも使えれば良いと思ったのですが、
複数オーガニゼーションで運用している場合、
apikey の切り替えが難しそうでしたので諦めてしまいました。

9日目の担当は、@norisu0313 さんの、
http://qiita.com/norisu0313/items/8e66f6b6adae60279d5f です!

mackerel-agent を root 以外で動かす

執筆時の mackerel-agent の version は、
mackerel-agent version 0.25.0 (rev 0ce0115) [linux 386 go1.4.2] です。

RHEL 系の Linuxmackerel-agentroot 以外で動かす方法です。
mackerel-agent を動かすユーザは mackerel とします。

設定

以下のコマンドを実行してユーザとディレクトリを作成します。

useradd -r mackerel
mkdir /var/log/mackerel-agent /var/run/mackerel-agent
chown mackerel: /var/log/mackerel-agent /var/run/mackerel-agent

/etc/init.d/mackerel-agent を以下のように変更します。

24,25c24,25
< LOGFILE=${LOGILE:="/var/log/$prog.log"}
< PIDFILE=${PIDFILE:="/var/run/$prog.pid"}
---
> LOGFILE=${LOGILE:="/var/log/mackerel-agent/$prog.log"}
> PIDFILE=${PIDFILE:="/var/run/mackerel-agent/$prog.pid"}
26a27
> USER="mackerel"
41c42
<     $BIN ${APIBASE:+--apibase=$APIBASE} ${APIKEY:+--apikey=$APIKEY} --pidfile=$PIDFILE --root=$ROOT $OTHER_OPTS >>$LOGFILE 2>&1 &
---
>     runuser -c "$BIN ${APIBASE:+--apibase=$APIBASE} ${APIKEY:+--apikey=$APIKEY} --pidfile=$PIDFILE --root=$ROOT $OTHER_OPTS >>$LOGFILE 2>&1" $USER &

変更したら、 service mackerel-agent restartmackerel ユーザで動作させることができます。

解説

軽く何をしているか書くと、

  • mackerel ユーザを作成
  • mackerel ユーザで pid と log を書き込めるようにし
  • runusermackerel ユーザで mackerel-agent を実行

しています。
runuser の代わりに su でも動作すると思います。
runuser は入っていない環境もあるようですので、適宜使い分けると良いです。

まとめ

mackerel-agentroot 以外で動かす方法を紹介しました。
CPU や Memory Usage のメトリクス収集は問題なく動作していましたが、
正常に動作しないことも考えられますのでご注意ください。
RHEL 以外のディストリビューションについても、
pid と log を書き込めるようにして、 runuser (su) 経由で実行すれば動作すると思います
(useraddadduser などの細かい違いもあるかもしれません)。
他には、systemdsupervisord で起動するという方法もありそうです。

Deploy ツール Stretcher で Rollback する

Stretcher を使うと、Consul と連携して、所謂 Pull 型の Deploy ができるようになります。

Consul と連携させる場合は、

$ consul event -name deploy s3://xxx-stretcher-files/deploy-20151112-193139.yml

のように、consul event で Manifest の path を指定してイベントを送ると、
YAML に書いてある path(s3, http, file) から tar.gz を取得して展開してくれます。
Stretcher は Rollback するのも簡単で、
Rollback したいバージョンの Manifest の path を指定して consul event を実行するだけです。
S3 に置いている場合、都度 aws s3 ls などして Manifest の path を調べて consul event を実行するのは面倒です。

そこで、以下の様なスクリプトを用意しておくと Rollback が楽になります。

aws s3 ls の結果から最新の Manifest を除いて、
bash の select で番号で選択できるようにしています。
実行すると以下のようになります。

$ ./rollback_stretcher.sh
 1) deploy-20151112-191452.yml  14) deploy-20151029-215027.yml
 2) deploy-20151112-191236.yml  15) deploy-20151028-182546.yml
 3) deploy-20151112-190934.yml  16) deploy-20151028-175655.yml
 4) deploy-20151112-190820.yml  17) deploy-20151028-175201.yml
 5) deploy-20151112-190421.yml  18) deploy-20151028-174909.yml
 6) deploy-20151112-190254.yml  19) deploy-20151028-174155.yml
 7) deploy-20151112-190127.yml  20) deploy-20151028-171838.yml
 8) deploy-20151112-190018.yml  21) deploy-20151022-191616.yml
 9) deploy-20151112-185832.yml  22) deploy-20151022-143736.yml
10) deploy-20151112-185441.yml  23) deploy-20151022-143011.yml
11) deploy-20151112-185341.yml  24) deploy-20151022-142645.yml
12) deploy-20151112-185242.yml  25) deploy-20151022-141728.yml
13) deploy-20151110-162450.yml  26) deploy-20151022-140614.yml
Select manifest (quit q or Ctrl-C) : 1
Event ID: 7a5e80c5-27aa-973e-3328-b7093a91439f

Rollback するスクリプトを用意しておくと、
障害発生時にも落ち着いて作業ができるので良いですね。

※前提条件として、Manifest のファイル名に timestamp を含めて sort できるようにしておく必要があります。

AWS Lambda のスケジュールイベントで定期的に RDS の Snapshot を作成する

AWS Lambda でスケジュールイベントの設定ができるようになりました。

これにより cron で実行していたスクリプトを Lambda に移行できます。
cron の冗長化は面倒ですし、cron のためだけに EC2 を起動しなくて良いので便利ですね。

そこで(?)、Lambda から AWSAPI を叩いて定期的に RDS の Snapshot を作成してみます。

Create Lambda Function

  • 「Get started now」か 「Create Lambda Function」を押下

Select blueprint

Lambda のテンプレートを選択します。

f:id:tkuchiki:20151106111308p:plain

  • Python 2.7 を選択
  • 「lambda-canary」か「hello-world-python」を選択
    • (一通り設定した後にスケジュールイベントの設定をしたかったので「hello-world-python」を選択)

Configure function

Lambda の設定をします。

f:id:tkuchiki:20151106112043p:plain

  • Name, Description を入力

Lambda function code

Lambda のコードを作成します。

f:id:tkuchiki:20151106112138p:plain

  • 以下のコードを入力する
    • lambda_handler の以下の変数を適宜修正
      • delete_days : 指定日経過したら削除する
        • 削除する必要がなければ delete_snapshots ごと削除
      • snapshot_prefix : Snapshot 名の prefix
      • instances : RDS の Instance 名を配列で指定
    • Snapshot 名の時刻は、JST に変換する必要がなければ tz=JST() を削除

追記 2017-01-11 14:09
Aurora の場合は以下のコードで snapshot を取ることができます。

Lambda function handler and role

Lambda function handler はソースコード中の関数を指定します。
handler を変更すれば lambda_handler 以外の関数を実行することも可能です。
また、Lambda 実行に必要な IAM role も設定します。
こちらもテンプレートがありますが、
RDS の Snapshot を操作するものは用意されていないので、
IAM policy を修正します。

f:id:tkuchiki:20151106112555p:plain

  • 「Basic execution role」を押下
    • IAM role 設定画面に遷移する
      • ポップアップブロックしていると設定できないようです

f:id:tkuchiki:20151106112839p:plain

  • 「編集」を押下

f:id:tkuchiki:20151106113006p:plain

  • (ドキュメントを読んで)「OK」を押下
  • 以下の json を入力する

Advanced settings

メモリやタイムアウトの時間を設定します。
数台分の Snapshot を作るだけの想定ですので、
タイムアウトは 10 秒で設定します。

f:id:tkuchiki:20151106113555p:plain

Review

  • 確認して「Create function」を押下

スケジュールイベントの設定

スケジュールイベントの設定を行います。
cron の書式が若干違うので、
詳しくは以下のドキュメントを参照してください。

http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/getting-started-scheduled-events.html

(執筆時は、)設定する時間は UTC ですので、
日本時間で設定しないように注意してください。

f:id:tkuchiki:20151106114226p:plain

  • 「Event sources」タブの「Add event source」を押下

f:id:tkuchiki:20151106113755p:plain

  • 「Scheduled Event」を押下

f:id:tkuchiki:20151106114508p:plain

  • Name, Description を入力
  • Schedule expression を入力
    • (cron(0 4 * ? * *))
      • (例は日本時間で毎13:00 に実行)

f:id:tkuchiki:20151106115118p:plain

これで、スケジュールイベントの設定完了です。
あとは、時間になれば対象 Instance の Snapshot が作成されます。

CloudWatch

CloudWatch に実行ログが出力されます。

f:id:tkuchiki:20151106115250p:plain

  • ログを選択

f:id:tkuchiki:20151106115328p:plain

print などで出力した結果やエラーログが出力されます。

まとめ

AWS Lambda で RDS の Snapshot を作成する方法を紹介しました。
EC2を使わないようにできるので、
可能な範囲で Lambda で実行させるようにすると運用が楽になりそうです。
また、紹介したコードでは省略しましたが、
実際に使う場合は Snapshot 作成に成功・失敗したかを、
チャットなどに通知するようにしたほうが良いですね。

Nginx で query string を見て動的にファイルを配信する

server {
    listen 80;
    server_name localhost;

    rewrite_log on;
    error_log /var/log/nginx/rewrite.log notice;

    location ~ ^/weather+\.json {
        rewrite ^ /weather/$arg_date.json;
    }

    location  ~ ^/weather/.*\.json {
        default_type application/json;
        root /var/www/html;
        try_files $uri =404;
        error_page 404 = @info;
    }

    location @info {
        more_set_headers -s 404 "Cache-Control: no-cache, no-store";
        return 404 " ";
    }
}

という設定ファイルで、

$ cat /var/www/html/weather/20141001.json
{"weather":"sunny"}

を置いている状態で、http request を送ると、

$ curl -s "http://localhost/weather.json?date=20141001"
{"weather":"sunny"}

$ curl -I "http://localhost/weather.json?date=20141001"
HTTP/1.1 200 OK
Server: ngx_openresty/1.4.3.6
Date: Fri, 10 Oct 2014 07:25:05 GMT
Content-Type: application/json
Content-Length: 20
Last-Modified: Fri, 10 Oct 2014 07:02:02 GMT
Connection: keep-alive
ETag: "5437846a-14"
Accept-Ranges: bytes

のようになります。
以下は、rewrite log です。

2014/10/10 16:08:03 [notice] 21659#0: *1 "^" matches "/weather.json", client: 127.0.0.1, server: localhost, request: "GET /weather.json?date=20141001 HTTP/1.1", host: "localhost"
2014/10/10 16:08:03 [notice] 21659#0: *1 rewritten data: "/weather/20141001.json", args: "date=20141001", client: 127.0.0.1, server: localhost, request: "GET /weather.json?date=20141001 HTTP/1.1", host: "localhost"

ファイルが存在しない場合は以下のように 404 を返すことが確認できます。

$ curl -I "htp://localhost/weather.json?date=20141002"
HTTP/1.1 404 Not Found
Server: ngx_openresty/1.4.3.6
Date: Fri, 10 Oct 2014 07:25:32 GMT
Content-Type: application/octet-stream
Content-Length: 1
Connection: keep-alive
Cache-Control: no-cache, no-store