右往左往ブログ

日々よりみち

最近のジョブスケジューラは何がいいのか

仕事でJenkinsを使ってるのですが、Jenkinsをビルドツール以上に、「定期実行してくれるもの全て」を管理するジョブ管理ツールとして使うことに疑問を覚えたので、メモします。

Jenkinsは継続的インテグレーションツールであって、ジョブ管理ツールではないと思うのです。

巷の記事を見ると、大抵「Jenkinsはジョブ管理の代替になる」という論調ばかりなのですが、Jenkinsをジョブ管理ツールとして使うには、かなり無理があるのではないかと思います。

というのも、ジョブ管理ツールに求めるのは

  • どのサーバで動いているか
  • 同一時刻にどんな処理が動いてるか
  • どのジョブとどのジョブが関連しているか

といった情報を設定でき、管理できることなのですが、1つ目はそもそもJenkins単体だとローカルでしか動かないので把握できない(sshなどでリモート実行は当然可能だが、それは処理を見ないと分からない)、2つ目は過去の実行履歴としては見られるが、定義としては俯瞰できない、3つ目はBuild Flow pluginBuild Pipeline pluginとしては実現可能ですが、細かいところで微妙に使いづらいのです。(Build Flowは、ジョブ間の繋がりを1つの定義ビューからしか追えないことや、実行後でないとgraphとして見ることができない点。Build Pipelineは、同一ジョブを複数のpipelineで使いまわせない点や、ジョブの"合流"ができない点)

確かにcronよりは遥かにマシですが、ジョブ管理ツールとして比べるにはかなり無理があるような気がしてなりません。そもそもJenkinsは"Build"というように、ビルドに特化したツールだと思うので、上記のような機能が充実してなくても違和感はないのですが、むしろ世間の「Jenkinsはジョブ管理として使える」という風潮は本当にそうなのだろうか?と思ったりします…。

一応ジョブ管理のツールとしては以下のようなものがあるようなのですが、とはいえこのへんのツールが最近の勉強会で取りざたされることはないような気がします(知らないだけかもしれません)。

qiita.com

世の中の企業はどうやってジョブ管理してるんでしょうか?

consul watchの変な挙動

watchとは

consulのwatch機能は、サービスなどを監視し、状態が変化したときに指定した処理を実行する機能です。

公式によると、以下の条件でwatchは発動するようです。 https://www.consul.io/docs/agent/watches.html

  • Key/Valueが変化したとき
  • Key Prefix配下が変化したとき
  • Serviceの数が変化したとき
  • nodeの数が変化したとき
  • 特定のServiceの状態が変化したとき
  • checkの状態が変化したとき
  • ユーザの指定したeventを発行したとき

ここではwatch機能の挙動を確認してみます。
自分の理解が足りないのか、ちょっと今のままでは使いづらいなという印象です…。
なお、検証したのは上記のうち「特定のServiceの状態が変化したとき」についてです。

使いづらいポイント

検証で後述しますが、まとめると以下です。

  • consulノードが起動/停止しても発動(チェック対象のサービスの状態にかかわらず)
  • consul reloadしても発動
    • しかも発動する回数が1回とは限らない
  • Serviceが停止/起動しても発動
    • しかも停止時と起動時でアクションを変更できない(どちらも同じアクションになる)

設定

今回はserver :1 , agent :2の構成で確認します。

基本設定

dc1_server

  • /etc/consul/conf.d/config.json
{
  "datacenter": "dc1",
  "data_dir": "/tmp/consul",
  "server": true,
  "bootstrap_expect": 1
}

consul自体の起動は、後でserviceとwatchのconfigを配置してから一緒に行います。

dc1_client[12]

2台登録します。(あまり今回の検証と関係ありませんが)

  • /etc/consul/conf.d/config.json
{
  "datacenter": "dc1",
  "data_dir": "/tmp/consul",
  "server": false,
  "retry_join": ["dc1server"]
}

Serviceの登録

dc1_server, dc1_client[12]

-config-dir配下にファイルを配置します。

  • /etc/consul/conf.d/service-filecheck.json
{
  "service": {
    "id": "file-check",
    "name": "file-check",
    "tags": ["master"],
    "checks": [
      {
        "script": "/tmp/filecheck.sh",
        "interval": "10s"
      }
    ]
  }
}

service監視用のスクリプトを配置します。

  • /tmp/filecheck.sh
#!/bin/bash

if [ -f "/tmp/testfile" ] ; then
  exit 0
fi
exit 2

単純にファイルの存在をチェックし、結果を返すだけのスクリプトです。 /tmp/testfile自体は空です。

watchの登録

dc1_server

/etc/consul/conf.d/watch-filecheck.json

{
  "watches": [
    {
      "type": "service",
      "service": "file-check",
      "handler": "/tmp/filecheck_handler.sh"
    }
  ]
}

起動

dc1_server, dc1_client[12]

consul agent -config-dir=/etc/consul/conf.d

検証

起動時にまず1回以上実行されます。
これは、3台を同時に起動しており、consul起動とService登録が順次行われたためと思われます。
起動順によるのか、1回だったり以下のとおり2回だったり、回数は不定です。
発動自体は想定通りなのですが、回数が不定なのは仕方ないとはいえちょっと気になります。

[root@dc1server /]# cat /tmp/consul.log
executed script: Sat Jun 27 06:14:37 UTC 2015
executed script: Sat Jun 27 06:14:42 UTC 2015

reloadしても1回以上実行されます。

[root@dc1server /]# consul reload
Configuration reload triggered
[root@dc1server /]# cat /tmp/consul.log
executed script: Sat Jun 27 06:14:37 UTC 2015
executed script: Sat Jun 27 06:14:42 UTC 2015
executed script: Sat Jun 27 06:15:23 UTC 2015 # 増えた
executed script: Sat Jun 27 06:15:23 UTC 2015 # 増えた

同じことを実行しても、発動が1回だけのときもあります。
これがよく分からない…。

[root@dc1server /]# consul reload
Configuration reload triggered
[root@dc1server /]# cat /tmp/consul.log
executed script: Sat Jun 27 06:14:37 UTC 2015
executed script: Sat Jun 27 06:14:42 UTC 2015
executed script: Sat Jun 27 06:15:23 UTC 2015
executed script: Sat Jun 27 06:15:23 UTC 2015
executed script: Sat Jun 27 06:15:48 UTC 2015 # 増えた

これは本来の動作ですが、サービスをダウンさせても発動します。
(ここではサービス監視対象のファイルを削除しています)

[root@dc1server /]# consul exec -node dc1client1 "rm -f /tmp/testfile"
==> dc1client1: finished with exit code 0
1 / 1 node(s) completed / acknowledged
[root@dc1server /]# consul exec "ls -l /tmp/testfile"
    dc1server: -rw-r--r-- 1 root root 0 Jun 27 04:34 /tmp/testfile
    dc1server:
==> dc1server: finished with exit code 0
    dc1client1: ls: cannot access /tmp/testfile: No such file or directory
    dc1client1:
    dc1client2: -rw-r--r-- 1 root root 0 Jun 27 04:34 /tmp/testfile
    dc1client2:
==> dc1client1: finished with exit code 2
==> dc1client2: finished with exit code 0
3 / 3 node(s) completed / acknowledged
[root@dc1server /]# cat /tmp/consul.log
executed script: Sat Jun 27 06:14:37 UTC 2015
executed script: Sat Jun 27 06:14:42 UTC 2015
executed script: Sat Jun 27 06:15:23 UTC 2015
executed script: Sat Jun 27 06:15:23 UTC 2015
executed script: Sat Jun 27 06:15:48 UTC 2015
executed script: Sat Jun 27 06:23:42 UTC 2015 # 増えた

サービスを復旧させても発動します。
(ここではサービス監視対象のファイルを作っています)

[root@dc1server /]# consul exec -node dc1client1 "touch /tmp/testfile"
==> dc1client1: finished with exit code 0
1 / 1 node(s) completed / acknowledged
[root@dc1server /]# consul exec "ls -l /tmp/testfile"
    dc1server: -rw-r--r-- 1 root root 0 Jun 27 04:34 /tmp/testfile
    dc1server:
==> dc1server: finished with exit code 0
    dc1client1: -rw-r--r-- 1 root root 0 Jun 27 06:25 /tmp/testfile
    dc1client1:
    dc1client2: -rw-r--r-- 1 root root 0 Jun 27 04:34 /tmp/testfile
    dc1client2:
==> dc1client2: finished with exit code 0
==> dc1client1: finished with exit code 0
3 / 3 node(s) completed / acknowledged
[root@dc1server /]# cat /tmp/consul.log
executed script: Sat Jun 27 06:14:37 UTC 2015
executed script: Sat Jun 27 06:14:42 UTC 2015
executed script: Sat Jun 27 06:15:23 UTC 2015
executed script: Sat Jun 27 06:15:23 UTC 2015
executed script: Sat Jun 27 06:15:48 UTC 2015
executed script: Sat Jun 27 06:23:42 UTC 2015
executed script: Sat Jun 27 06:25:32 UTC 2015 # 増えた

Service機能の1つであるDNSは、想定通り3つ返しています。

[root@dc1server /]# dig @127.0.0.1 -p 8600 file-check.service.consul. ANY
(snip)

;; QUESTION SECTION:
;file-check.service.consul. IN  ANY

;; ANSWER SECTION:
file-check.service.consul. 0    IN  A   172.17.0.34
file-check.service.consul. 0    IN  A   172.17.0.36
file-check.service.consul. 0    IN  A   172.17.0.32

(snip)

まとめ

consulのservice機能自体はかなり便利です。特に生きているノードに対してのみレコードを返すようなDNSは、いろいろと応用が効きそうです。

一方、Serviceと組み合わせたwatch機能はかなり使いづらい印象を受けました。

  • Serviceのdown/upに関係なく発動する
  • configのreload時にも発動する
  • 1回発動するとは限らない。何回発動するかも分からない(自分だけかも)

watch機能は、何回実行しても問題ないような処理に限ったほうがよさそうだという印象です。configの入れ替えなどでしょうか。ただ、サービスのダウン時、アップ時に処理を分けたいと思うことは多いと思われますので、今のままだとスクリプトで処理を分けなければならず、ちょっと使いづらいです。特にreload時に勝手に発動するのは、できればコントロールさせて欲しい…。

以下でも話題になっています。
Reloading Consul re-runs all watch commands every time. · Issue #571 · hashicorp/consul · GitHub

あと今回は触れませんでしたが、checkと連動するときも注意が必要です。 「このcheck」という指定ができないので、checkを複数種類登録しており、かついずれかのcheckの状態が変更されると、どのcheckだったとしてもwatchが発動します。 基本的に、watchはserviceに紐づくものなので、checkもserviceに紐付けることが前提になりそうです。

consul : aclを利用して特定のdcからのみconsul execを実行可能なようにアクセス制限をかける

consul exec

consulにはconsul execという便利なコマンドがあります。
これは、consulクラスタに属しているメンバに対して一斉にコマンドを発行できるというものです。 しかも、発行先を選択することができ、データセンタ単位やノード単位だったり、それを正規表現で更に絞ったりと柔軟な発行ができます。

このように、consul execは非常に便利なコマンドである一方、権限によっては何でもできてしまうので、限られた条件でしか発行できないように 制限をかけることも考えたくなります。

一方、consulにはacl機能もあり、tokenを発行してこのような権限管理を行うことができます。

aclを利用した制限のかけ方(範囲)には様々なケースが考えられます。
またconsulにはdatacenterという考え方がありますが、dcを管理エリア単位と捉えて、以下のようなイメージで設定を行ってみます。

  • dc1 : マネジメントdc。このdcからは他のdcも含めてどこにでもexecを発行できる。
  • dc2 : 非マネジメントdc。このdcからはconsul execを発行した場合、dc1にもdc2にも発行できない。

イメージとしては以下のとおりです。

f:id:unchemist:20150621015333p:plain

この後に設定を行っていきますが、「擬似的に」その状態にすることは可能です。(どう「擬似的」なのかは後述します)

consul execの仕組み

https://www.consul.io/docs/commands/exec.html https://www.consul.io/docs/internals/acl.html

公式のページにもありますが、consul execは以下のような仕組みで実行されています。

  • consulのkey/valueストア機能を利用
    • デフォルトで_rexecprefixに対してread/writeを行い、この値をノード間で伝播させてそれぞれのノードでコマンドを発行している
      したがって、_rexecprefixに対して読み書きできないとconsul execは実行できない。
      prefixである_rexecという値はコマンド発行時に変更可能。
  • acl的には、デフォルトではanonymous tokenを利用してkey/valueの読み書きを行う。consul execも同様。
    利用するtokenのデフォルトはanonymousだが、acl_tokenの設定により変更することが可能。

基本的な動きは前回のエントリで紹介したので、ここでは設定だけ。

設定

dc1 server

consul agent -config-file=/etc/consul/conf.d/dc1_server.json

/etc/consul/conf.d/dc1_server.json

{
  "datacenter": "dc1",
  "data_dir": "/tmp/consul",
  "server": true,
  "bootstrap_expect": 1,
  "acl_datacenter": "dc1",
  "acl_default_policy": "deny",
  "acl_master_token": "master_token",
  "acl_token": "anonymous"
}

/tmp/acl.json

{
  "ID": "anonymous", 
  "Type": "client", 
  "Rules": "{
    \"key\": {
      \"_somerandomstring/\": { 
        \"policy\":\"write\"
      }
    }
  }" 
}

設定

[root@dc1server /]# curl -X PUT http://localhost:8500/v1/acl/update?token=master_token -d @/tmp/acl.json
{"ID":"anonymous"}

dc1 client

consul agent -config-file=/etc/consul/conf.d/dc1_client.json -join=dc1server

/etc/consul/conf.d/dc1_client.json

{
  "datacenter": "dc1",
  "data_dir": "/tmp/consul",
  "server": false,
  "acl_token": "anonymous"
}

dc2 server

consul agent -config-file=/etc/consul/conf.d/dc2_server.json -join-wan=dc1server

/etc/consul/conf.d/dc2_server.json

{
  "datacenter": "dc2",
  "data_dir": "/tmp/consul",
  "server": true,
  "bootstrap_expect": 1,
  "acl_datacenter": "dc1",
  "acl_token": "anonymous"
}

dc2 client

consul agent -config-file=/etc/consul/conf.d/dc2_client.json -join=dc2server

/etc/consul/conf.d/dc2_client.json

{
  "datacenter": "dc2",
  "data_dir": "/tmp/consul",
  "server": false,
  "acl_token": "anonymous"
}

結果

consul exec実行時は、実行オプションに-prefix="_somerandomstring"を指定しないとエラーになります。

[root@dc1server /]# consul exec hostname
Failed to create job file: Unexpected response code: 403 (Permission denied)
[root@dc1server /]# consul exec -prefix="_somerandomstring" hostname
    dc1server: dc1server
    dc1server:
==> dc1server: finished with exit code 0
    dc1client: dc1client
    dc1client:
==> dc1client: finished with exit code 0
2 / 2 node(s) completed / acknowledged
[root@dc1server /]# consul exec -prefix="_somerandomstring" -datacenter="dc2" hostname
    dc2server: dc2server
    dc2server:
==> dc2server: finished with exit code 0
    dc2client: dc2client
    dc2client:
==> dc2client: finished with exit code 0
2 / 2 node(s) completed / acknowledged

仕組み

仕組みは単純で、consul execで指定するk/vのprefixを明示的に指定し、そのprefixに対してのみwrite権限を付与するというものです。
そのprefix自体を秘匿してしまうことで、"結果的に"dc1でしかconsul execを発行できなくなります。

  • _somerandomstringという文字列のみwriteを許可しているので、この文字列を推測不能にしておく必要があります。
  • _somerandomstringという文字列を見るためには、dc1のmaster tokenを利用してaclの中を覗き見るしかありません。
    が、dc2から見るとacl_datacenterdc1となっているため、dc2内のサーバにはmaster tokenの設定はありません。
  • 逆に言うと、_somerandomstringを知られてしまい、それを使えば、dc2からであっても実行は可能です。

最後に

必要なのでこういう検証をしたわけですが、そもそもこんな使い方をするところはないような気がします。
あと、結構無理やり設定を活用した気がしてならないので、もっと簡単にできればいいのですが…。

consulのacl管理とconsul execの関係

consulのacl

consulは、aclによって実行制限を行うことができます。
ここでは、multi datacenterでacl管理を行うための、設定と効果を見てみます。

きっかけは、consulの設定の中にacl_datacenterというものがあるのですが、ネット上のいろいろなページを見てもいまいち意味がわからなかったためです。 datacenteracl_datacenter を同じ値にしていたり、では別の値だとどうなるのか?など…。

consul接続イメージ

今回構築しているのは以下のようなイメージです。(前回と同じイメージです)

  • dc1,dc2の2つのデータセンタがあり、それぞれにconsulサーバ1台、consulクライアント1台が属する
  • dc1とdc2間は接続されている

f:id:unchemist:20150620190747p:plain

構築

前回の設定に加えて、aclの設定を入れてみます。

dc1 server

consul agent -config-file=/etc/consul/conf.d/dc1_server.json

/etc/consul/conf.d/dc1_server.json

{
  "datacenter": "dc1",
  "data_dir": "/tmp/consul",
  "server": true,
  "bootstrap_expect": 1
  "acl_datacenter": "dc1",
  "acl_default_policy": "deny",
  "acl_master_token": "master_token",
  "acl_token": "anonymous"
}

acl_default_policydenyとしています。

dc1 client

consul agent -config-file=/etc/consul/conf.d/dc1_client.json -join=dc1server

/etc/consul/conf.d/dc1_client.json

{
  "datacenter": "dc1",
  "data_dir": "/tmp/consul",
  "server": false,
  "acl_token": "anonymous"
}

dc2 server

consul agent -config-file=/etc/consul/conf.d/dc2_server.json -join-wan=dc1server

/etc/consul/conf.d/dc2_server.json

{
  "datacenter": "dc2",
  "data_dir": "/tmp/consul",
  "server": true,
  "bootstrap_expect": 1,
  "acl_datacenter": "dc1",
  "acl_default_policy": "allow",
  "acl_master_token": "master_token",
  "acl_token": "anonymous"
}

ここでは、datacenterdc2ですが、acl_datacenterdc1としています。
また、acl_default_policyallowとしています。

dc2 client

consul agent -config-file=/etc/consul/conf.d/dc2_client.json -join=dc2server

/etc/consul/conf.d/dc2_client.json

{
  "datacenter": "dc2",
  "data_dir": "/tmp/consul",
  "server": false,
  "acl_token": "z"
}

他のノードと異なり、acl_token名を異なるものにしています。
(anonymousではなくsome_tokenとしています)

各設定の意味

  • acl_datacenter:
    自dcが従いたいdc名を指定します。ここで設定したdcでのacl設定の影響を受けます。
    他dcを指定した場合、自dcでのacl設定は意味を成さなくなり、指定したdcのacl設定に従います。
  • acl_default_policy:
    acl設定が、blacklistモードかwhitelistモードかを指定します。
    allowと指定した場合、基本的にあらゆる操作を許容し、別途拒否したい操作を追加で設定していきます。
    denyと指定した場合、逆に基本的にあらゆる操作を拒否し、別途許容したい操作を追加で設定していきます。
  • acl_master_token:
    key/valueに対して、read/write(=あらゆる操作)が可能になるtoken名を指定します。
  • acl_token:
    tokenを指定しない場合に、デフォルトで(暗黙的に)指定するtokenを記載します。

consul exec実行によるaclの影響確認

consul exec コマンドも、暗黙的にk/vで_rexecprefixを利用しているため、aclの影響を受けます。
上記設定の場合、どのような振る舞いになるのか確認してみます。

dc1 server

[root@dc1server /]# consul exec hostname
Failed to create job file: Unexpected response code: 403 (Permission denied)

dc2 server

[root@dc2server /]# consul exec hostname
Failed to create job file: Unexpected response code: 403 (Permission denied)

ポイントは、dc2 serverではconfigで"acl_default_policy": "allow"と設定しているにも関わらず、実行に失敗している点です。
これは、acl_datacenterdc1であるために、dc1側の設定である"acl_default_policy": "deny"が有効になっているためと思われます。

aclの設定

ここから、acl設定を追加することでどのような挙動になるのかを確認してみます。
最終的には、consul execが実行でき、結果が返ってくるようにします。

aclの登録

まずはpolicy設定を作成します。 公式(https://www.consul.io/docs/internals/acl.html)によるとjson形式で記述できるとあるのですが、少しだけ注意が必要です。 下記を見るとわかる通り、jsonjsonなのですが、jsonの中にjsonを書くという、特殊な記載になります…。
(全体がjson形式で、さらにRulesの中が更にjsonになっていることがわかると思います)

/tmp/acl.json

{
  "ID": "anonymous", 
  "Type": "client", 
  "Rules": "{
    \"key\": {
      \"_rexec/\": { 
        \"policy\":\"write\"
      }
    }
  }" 
}

このjsonファイルを登録します。登録時はacl_master_tokenで指定したtokenを指定します。

[root@dc1server /]# curl -X PUT http://localhost:8500/v1/acl/update?token=master_token -d @/tmp/acl.json
{"ID":"anonymous"}

効果の確認

先ほど、anonymous tokenの_rexec prefixに対してwrite権を付与したので、consul execが実行できるはずです。
まずはdc1 serverから。

[root@dc1server /]# consul exec hostname
    dc1server: dc1server
    dc1server:
==> dc1server: finished with exit code 0
    dc1client: dc1client
    dc1client:
==> dc1client: finished with exit code 0
2 / 2 node(s) completed / acknowledged
[root@dc1server /]# consul exec -datacenter="dc2" hostname
    dc2server: dc2server
    dc2server:
==> dc2server: finished with exit code 0
1 / 1 node(s) completed / acknowledged
[root@dc1server /]#

実行できています。ただし、dc2 clientの結果は返ってきていません。
次にdc2 serverから。

[root@dc2server /]# consul exec hostname
    dc2server: dc2server
    dc2server:
==> dc2server: finished with exit code 0
1 / 1 node(s) completed / acknowledged
[root@dc2server /]# consul exec -datacenter="dc1" hostname
    dc1server: dc1server
    dc1server:
==> dc1server: finished with exit code 0
    dc1client: dc1client
    dc1client:
==> dc1client: finished with exit code 0
2 / 2 node(s) completed / acknowledged

上記の通り、実行できました。ただし、こちらもdc2 clientだけ結果が返って来ません。
これは、dc2 clientのacl_tokenanonymousではなく別の名前(ここではsome_token)にしているためです。
先ほどaclの設定でwrite権を設定した対象は、anonymousでした。

dc2 clientでも以下のログが出力されています。

2015/06/20 15:09:24 [ERR] agent: failed to get remote exec job: rpc error: ACL not found

aclの登録(2回目)

そこで、some_tokenに対しても_rexecに対してwrite権を付与してみます。

/tmp/acl_2.json

{
  "ID": "some_token",
  "Type": "client",
  "Rules": "{
    \"key\": {
      \"_rexec/\": {
        \"policy\":\"write\"
      }
    }
  }" 
}

登録します。

[root@dc1server /]# curl -X PUT http://localhost:8500/v1/acl/create?token=master_token -d @/tmp/acl_2.json
{"ID":"some_token"}

最初と異なり、今回のhttp apiupdateではなくcreateを指定します。これは、anonymousはデフォルトで既存の設定が付与されているためです。

aclcreateは、通常はIDを指定せずに実行することで、ランダム文字列となるtokenが返ってきます。
通常、httpのapiを利用するときには、ここで返却されるIDを利用してtokenに指定することで権限を利用して操作を行いますが、
今回はacl_tokenで既にtoken名を指定してしまっているので、決め打ちでaclを設定しています。

効果の確認(2回目)

これで、dc2 clientでもconsul execが実行できているはずです。

[root@dc1server /]# consul exec -datacenter="dc2" hostname
    dc2server: dc2server
    dc2server:
==> dc2server: finished with exit code 0
    dc2client: dc2client
    dc2client:
==> dc2client: finished with exit code 0
2 / 2 node(s) completed / acknowledged

無事に実行できました。

これで、aclの効果と設定方法が少しだけ分かった気がします。

consulのdatacenter間接続

consulをいろいろと検証してみているので、分かったことをまとめます。
consulのversionは0.5.2です。

consulとは

consulとはservice検出、healthcheck機能やkey/value store機能を持ったクラスタリングソフトです。

https://www.consul.io/

公式によると以下。

  • サービス検出
  • ヘルスチェック
  • Key/Valueストア
  • マルチデータセンタ

consul接続イメージ

今回構築しているのは以下のようなイメージです。

  • dc1,dc2の2つのデータセンタがあり、それぞれにconsulサーバ1台、consulクライアント1台が属する
  • dc1とdc2間は接続されている

f:id:unchemist:20150620190747p:plain

このようなときに、どのような見え方になるのか?を確認してみます。

構築

dc1 server

consul agent -config-file=/etc/consul/conf.d/dc1_server.json

/etc/consul/conf.d/dc1_server.json

{
  "datacenter": "dc1",
  "data_dir": "/tmp/consul",
  "server": true,
  "bootstrap_expect": 1
}

dc1 client

consul agent -config-file=/etc/consul/conf.d/dc1_client.json -join=dc1server

/etc/consul/conf.d/dc1_client.json

{
  "datacenter": "dc1",
  "data_dir": "/tmp/consul",
  "server": false
}

dc2 server

consul agent -config-file=/etc/consul/conf.d/dc2_server.json -join-wan=dc1server

/etc/consul/conf.d/dc2_server.json

{
  "datacenter": "dc2",
  "data_dir": "/tmp/consul",
  "server": true,
  "bootstrap_expect": 1
}

dc2 client

consul agent -config-file=/etc/consul/conf.d/dc2_client.json -join=dc2server

/etc/consul/conf.d/dc2_client.json

{
  "datacenter": "dc2",
  "data_dir": "/tmp/consul",
  "server": false
}

consul membersでの見え方

dc1 serverから見た場合

[root@dc1server /]# consul members
Node       Address           Status  Type    Build  Protocol  DC
dc1client  172.17.0.58:8301  alive   client  0.5.2  2         dc1
dc1server  172.17.0.57:8301  alive   server  0.5.2  2         dc1
[root@dc1server /]# consul members -wan
Node           Address           Status  Type    Build  Protocol  DC
dc1server.dc1  172.17.0.57:8302  alive   server  0.5.2  2         dc1
dc2server.dc2  172.17.0.59:8302  alive   server  0.5.2  2         dc2

dc1 clientから見た場合

[root@dc1client /]# consul members
Node       Address           Status  Type    Build  Protocol  DC
dc1server  172.17.0.57:8301  alive   server  0.5.2  2         dc1
dc1client  172.17.0.58:8301  alive   client  0.5.2  2         dc1
[root@dc1client /]# consul members -wan

このことから、以下のことが分かります。

  • consul membersでは、自dc内のメンバを確認できる
  • -wanオプションを付与することで、dc同士のサーバ間接続については確認可能。clientからは確認不可。

consul execでの発行先

dc1 serverから実行した場合

[root@dc1server /]# consul exec hostname
    dc1server: dc1server
    dc1server:
==> dc1server: finished with exit code 0
    dc1client: dc1client
    dc1client:
==> dc1client: finished with exit code 0
2 / 2 node(s) completed / acknowledged
[root@dc1server /]# consul exec -datacenter="dc2" hostname
    dc2server: dc2server
    dc2server:
==> dc2server: finished with exit code 0
    dc2client: dc2client
    dc2client:
==> dc2client: finished with exit code 0
2 / 2 node(s) completed / acknowledged

dc1 clientから実行した場合

[root@dc1client /]# consul exec hostname
    dc1client: dc1client
    dc1client:
==> dc1client: finished with exit code 0
    dc1server: dc1server
    dc1server:
==> dc1server: finished with exit code 0
2 / 2 node(s) completed / acknowledged
[root@dc1client /]# consul exec -datacenter="dc2" hostname
    dc2server: dc2server
    dc2server:
==> dc2server: finished with exit code 0
    dc2client: dc2client
    dc2client:
==> dc2client: finished with exit code 0
2 / 2 node(s) completed / acknowledged

このことから、以下のことが分かります。

  • serverでもclientでもexecコマンドは実行可能。
  • clientから見てmembers -wanコマンドで他dcが見えなかったとしても、コマンド自体は発行できる。

f:id:unchemist:20150620190748p:plain

logrotateでcreate時にパーミッションが効かない

/etc/logrotate.d/ 配下でログローテートの設定を行ったときに、ローテートが実行されてもどうしてもパーミッション設定が変わらないことがありました。以下のような設定をしていたときです。

/etc/logrotate.d/sample

/var/log/hoge.log {
      daily
      rotate 7
      missingok
      copytruncate
      create 644 root root
}

もともと、対象のログのパーミッション640だったものを、ログローテート時に644にうまく変更してくれることを期待していました。 しかし、実際にはローテートしたとしても、パーミッションは変わりません。640で作成されたなら、640のままです。どうしても設定が効かずに、ならなぜcreateみたいな設定があるのだろうか?と疑問に思いました。

原因は、設定の競合でした。

  • copytruncate : ファイル自体を消さずに、中身をコピーして元ファイルの中身を消す
  • create : ファイルが存在しなければ作る

ということで、そもそもcreateは、ファイルをなければ作るのであって、既に存在するファイルのパーミッションを変更するものではないのでした。一方でcopytruncateは、ファイルの存在自体を消さないので、いつまでも当初のパーミッションのままファイル自体は存在し続けます。

従って、ログローテート対象のログのパーミッションを変更するには、以下のようにするしかなさそうです。

  • logrotateのオプションでcopytruncateをやめる
  • そもそもchmodなどでログのパーミッションを変更する

そもそも、ミドルウェアがローテート後のログを見続ける問題があるので、copytruncateを入れているのであれば、「あえて」入れている場合がほとんどだと思います。

ということで、素直に後者で対応しましょう、という話でした。

chefの依存定義(depends)を勘違いしていた話

chefでいうmetadata.rb中のdependsが保証する依存性がどこまでなのかを勘違いしていたのでメモしておきます。 結論からいうと、 depends指定はattributeの参照先として読み込むだけ の機能でした。

  • dependsにより保証されること
    • depends参照先cookbookのattributeを流用できること
  • dependsで保証されないこと
    • 必ずしもdepends参照先cookbookを実行する必要がないこと
    • ましてや、depends参照先が、記載元cookbookより「先に」実行される必要はないこと

この、「保証されないこと」ができるとばかり勘違いしていたので、dependsに頼った開発をしていたらエラーにならずに、あれ…?となってしまったのでした。

公式の記述

公式のmetadata.rb

以下のように記載があります。

Use to show that a cookbook has a dependency on another cookbook.

これだけだと分からないですね。

テスト

事前準備

cookbook aとcookbook bを作っておきます。

概ね以下のような構造です。(検証で無関係なところは省略しています)

    chef-repo
     ├cookbooks
     │├a
     ││├attributes/default.rb
     ││├recipes/default.rb
     ││└metadata.rb
     │└b
     │  ├attributes/default.rb
     │  ├recipes/default.rb
     │  └metadata.rb
     └localhost.json

cookbook bのmetadata.rb中でaへの依存性を定義します。

cookbooks/b/metadata.rb

<略>
depends          'a'

cookbook a,b内ではそれぞれattributeを定義しておきます。

cookbooks/a/attributes/default/rb

default['a']['value'] = "attribute a"

cookbooks/b/attributes/default/rb

default['b']['value'] = "attribute b"

cookbook bのrecipeでは、cookbook a,bのattributeを参照するようにします。

cookbooks/b/recipes/default.rb

log "this is cookbook b"
log "attribute b: #{node['b']['value']}"
log "attribute a: #{node['a']['value']}"

chef-solo実行時は、cookbook bのみを実行するようにします。

localhost.json

{
    "run_list":[
        "recipe[b]"
    ]
}

このまま実行する場合

depends指定をすれば、たとえ他cookbookを実行しなかったとしても、attributeは利用できます。

$ sudo chef-solo -c solo.rb -j ./localhost.json
Starting Chef Client, version 12.0.3
Compiling Cookbooks...
Converging 3 resources
Recipe: b::default
  * log[this is cookbook b] action write

  * log[attribute b: attribute b] action write

  * log[attribute a: attribute a] action write


Running handlers:
Running handlers complete
Chef Client finished, 3/3 resources updated in 1.688893613 seconds

depends指定をせず、他cookbookのattributeを参照する場合

attributeが参照できないので、当然エラーになります。

cookbooks/b/metadata.rb

<略>
# depends          'a' # コメントアウト

実行時エラー。

$ sudo chef-solo -c solo.rb -j ./localhost.json
Starting Chef Client, version 12.0.3
Compiling Cookbooks...

================================================================================
Recipe Compile Error in /home/vagrant/chef-repo/cookbooks/b/recipes/default.rb
================================================================================

NoMethodError
-------------
undefined method `[]' for nil:NilClass

Cookbook Trace:
---------------
  /home/vagrant/chef-repo/cookbooks/b/recipes/default.rb:12:in `from_file'

Relevant File Content:
----------------------
/home/vagrant/chef-repo/cookbooks/b/recipes/default.rb:

  5:  # Copyright 2015, YOUR_COMPANY_NAME
  6:  #
  7:  # All rights reserved - Do Not Redistribute
  8:  #
  9:
 10:  log "this is cookbook b"
 11:  log "attribute b: #{node['b']['value']}"
 12>> log "attribute a: #{node['a']['value']}"
 13:

<略>

attribute上の依存関係をなくして、依存先cookbookを利用しない場合

cookbook aのattributeを参照しないようにします。

cookbooks/b/recipes/default.rb

log "this is cookbook b"
log "attribute b: #{node['b']['value']}" 
# log "attribute a: #{node['a']['value']}" # コメントアウト

cookbook bのaへの依存性も元に戻しておきます。

cookbooks/b/metadata.rb

<略>
depends          'a'

この時点で実行すると、エラーは発生せず正常終了します。

$ sudo chef-solo -c solo.rb -j ./localhost.json
Starting Chef Client, version 12.0.3
Compiling Cookbooks...
Converging 2 resources
Recipe: b::default
  * log[this is cookbook b] action write

  * log[attribute b: attribute b] action write


Running handlers:
Running handlers complete
Chef Client finished, 2/2 resources updated in 41.895869267 seconds

ポイントは、 dependsによりcookbook aを指定しているにも関わらず、aを実行しなくても問題がない ことです。

依存先cookbookを配置しない場合

では、cookbook aをそもそも配置しなかったら…エラーになります。

chef-repo
 ├cookbooks
 ││# aディレクトリを消す
 │└b
 │  ├attributes/default.rb
 │  ├recipes/default.rb
 │  └metadata.rb
 └localhost.json

実行時エラー。

$ sudo chef-solo -c solo.rb -j ./localhost.json
Starting Chef Client, version 12.0.3
Compiling Cookbooks...

Running handlers:
[2015-03-29T00:11:22+00:00] ERROR: Running exception handlers
Running handlers complete
[2015-03-29T00:11:22+00:00] ERROR: Exception handlers complete
[2015-03-29T00:11:22+00:00] FATAL: Stacktrace dumped to /tmp/chef-solo/chef-stacktrace.out
Chef Client failed. 0 resources updated in 41.570472421 seconds
[2015-03-29T00:11:22+00:00] ERROR: Cookbook a not found. If you're loading a from another cookbook, make sure you configure the dependency in your metadata
[2015-03-29T00:11:23+00:00] FATAL: Chef::Exceptions::ChildConvergeError: Chef run process exited unsuccessfully (exit code 1)

まとめ

metadata.rb中のdepends指定により分かったことは以下です。

  • depends参照先のcookbookはcookbook_pathに配置している必要がある
  • ただし、配置するだけでよく、実行される必要はない

「cookbook aが実行されてからでないとcookbook bが実行されてはならない」というシチュエーション時に、dependsを記載すれば依存関係や順序性が(実行はされずとも)チェックされると思ったのですが、全然そんなことはなかったです…。例えば、「jdkをインストールしてからelasticsearchをインストール」とか、「rubyをインストールしてから各種gemをインストール」とか、そういったケースはあり得ると思うのですが、それはdependsでチェックするべきではなかったんですね…。

期待していたのは、

"recipe[gem]",
"recipe[ruby]"

みたいな書き方をlocalhost.json中で書いたとし、「rubyを先に実行する必要があるよ」とエラーを出してくれるとか、

"recipe[gem]"

と書いたときに「rubyが実行されてないよ」とエラーを出してくれることだったのですが、そうはならず…。

おそらく、ひとつのcookbookにinclude_recipeを記載することで順序性や依存性を定義するのが理想なのかと思いますが、それを汲み取ることができずに苦労した話でした。