-bootstrap-expect=3のConsulサーバクラスタが一旦落ちると復帰できないのでその対策

# 冗長化したConsulサーバクラスタでの問題

Consulはサーバを-bootstrap-expectオプションで3以上（あるいは、設定ファイルでbootstrap_expectの値を3以上）を指定して3台以上起動することによってサーバクラスタとして冗長化することができ、半数以上が停止するまでサーバとして利用可能な状態のままで居続けることができる。
つまり、-bootstrap-expectが3なら1台までサーバが落ちても問題ない。

逆に言えば半数以上のサーバが落ちてしまえば利用できなくなる。[^1]
もちろんそれは仕様通りなのだが、（少なくとも私の環境では）その後再度落ちたサーバを普通に起動してもLeaderの再選定が行われず全員Followerになってしまうため利用可能な状態になってくれない。
これはバグだと思われる。
https://github.com/hashicorp/consul/issues/993

# 復帰方法

そのような場合に復帰させる方法を試行錯誤して見つけ出したので紹介する。

* サーバのconsulを全部一旦落とす。
* -bootstrap-expectを1にしてサーバのconsulを全部起動する。
* そうするとLeaderが1台、Followerが1台、エラーが1台、という状態になる。
 * 各サーバ上で`consul exec - <<< 'consul info | grep Leader'`を実行して「Failed to create session: Unexpected response code: 500 (No cluster leader)」と返ってくればエラー、自分が「finished with exit code 0」ならばLeader、「finished with exit code 1」ならばFollower。
* まずエラーの1台のconsulを落とし、-bootstrap-expectを3にして起動し直す。続けてFollowerを落として-bootstrap-expectを3にして起動。最後にLeaderを落として-bootstrap-expectを3にして起動。
* 各クライアントのconsulを再起動。

# これをAnsibleで

この復帰方法をAnsibleのplaybookにしてみた。
前提として私の環境は、

* consulにはパスが通っている。
* http://qiita.com/yunano/items/7ef5fa5670721de55627 を使用してサービスとしてconsulを起動している。
* 設定ファイル /etc/consul.d/agent.json にbootstrap_expectを指定している。
* `consul members`の結果からAnsibleのdynamic inventoryを以下のように作っている。
 * Consulが死んでいるのが前提なので https://github.com/ansible/ansible/blob/devel/contrib/inventory/consul_io.py は使用していない（そもそも使ったことないのだが）。
 * その状態でも`consul members`は通るので利用した次第。

```sh:consul_members.sh
#!/bin/bash

hosts=$(consul members | sed '1d')
echo "{'members':["
<<< "$hosts" awk '{ print "\047" $1 ".node.consul\047" }' | paste -s -d,
echo "],'server':["
<<< "$hosts" awk '$4=="server" { print "\047" $1 ".node.consul\047" }' | paste -s -d,
echo "],'alive':["
<<< "$hosts" awk '$3=="alive" { print "\047" $1 ".node.consul\047" }' | paste -s -d,
echo "],'_meta':{'hostvars':{"
<<< "$hosts" awk '{ sub(":[0-9]+$","",$2); print "\047" $1 ".node.consul\047:{\047ansible_host\047:\047" $2 "\047}" }' | paste -s -d,
echo "}}}"
```

このシェルスクリプトの実行結果の例：

```
{'members':[
'c1.node.consul','s1.node.consul','s2.node.consul','s3.node.consul'
],'server':[
's1.node.consul','s2.node.consul','s3.node.consul'
],'alive':[
'c1.node.consul','s1.node.consul','s2.node.consul','s3.node.consul'
],'_meta':{'hostvars':{
'c1.node.consul':{'ansible_host':'10.18.1.10'},'s1.node.consul':{'ansible_host':'10.18.0.10'},'s2.node.consul':{'ansible_host':'10.18.0.11'},'s3.node.consul':{'ansible_host':'10.18.0.12'}
}}}
```

playbookは以下の通り。

```yaml:consul_restart.yml
- hosts: server
  tasks:
  - service: name=consul state=stopped
  - lineinfile: dest=/etc/consul.d/agent.json regexp='^( *"bootstrap_expect":)' line='\1 1' backrefs=yes
  - service: name=consul state=started
  - lineinfile: dest=/etc/consul.d/agent.json regexp='^( *"bootstrap_expect":)' line='\1 3' backrefs=yes
  - block:
    - shell: consul exec - <<< 'consul info | grep Leader'
      register: result
      retries: 3
      delay: 3
      until: result.rc != 1
      failed_when: result.rc == 1
    rescue:
    - service: name=consul state=restarted
  - service: name=consul state=restarted
    when: "'{{ inventory_hostname_short }}: finished with exit code 1' in result.stdout"
  - service: name=consul state=restarted
    when: "'{{ inventory_hostname_short }}: finished with exit code 0' in result.stdout"

- hosts: "!server"
  tasks:
  - service: name=consul state=restarted
```

Consulクライアント上でplaybookを実行。
dynamic inventoryのシェルスクリプトには実行権限を付けておく。
Consulサーバ上ではないのはサーバが正常になった時点で`consul members`からクライアントが見えないから。でもサーバもクライアントも全滅してしまっている状況からの復帰だとクライアント上だと`consul members`で自分しか見えないのでサーバ上で実行してクライアントは何か別の方法で再起動しないとならないが。

```
# chmod +x consul_members.sh
# ansible-playbook -i consul_members.sh consul_restart.yml
```

---

[^1]: 正確には、落ちたのがFollowerだけでLeaderが生き残り続けていけば半数以上が落ちたとしても利用可能な状態のまま居続けることができる。