Consul 0.5.0をAnsibleを使ってアップグレードしたらConsulが動かなくなったのでその対策

# シチュエーション

Consul serverが3台＋Consul client数台という状態で、Consul 0.5.0から0.5.1あるいは0.5.2へアップグレードするのに https://www.consul.io/docs/upgrading.html の「Standard Upgrades」に従って1台ずつConsul Serverのconsulプロセスを再起動する、という作業をAnsibleを使って実施したところ、Consulは起動はしているようだがまともに動いていなかった。
例えば`consul exec hostname`を実行すると`Failed to create session: Unexpected response code: 500 (No cluster leader)`というエラーメッセージが返ってきた。

0.5.1から0.5.2へのアップグレードを同じ方法で行うと問題は発生しなかった。

# 解説

どうやらleaderとなっているConsul serverが再起動されると再度leader選定が行われるが、選定が終わる前に別のConsul serverを再起動すると選定ができなくなってしまうようだ。

https://www.consul.io/docs/upgrade-specific.html にある通り、0.5.0から0.5.1以降にアップグレードするとデータ用DBのマイグレーションが発生する影響で普段のアップグレードよりleader選定に多少時間が掛かる。
そのため0.5.0から0.5.1以降へのアップグレードでは1台再起動したら即、次の1台を再起動する、ということをやっているとleader選定ができなくなってConsulクラスタ全体が死ぬ。

0.5.1から0.5.2へのアップグレードでは1台再起動したら即、次とやっていっても選定が間に合うので問題が発生しなかったと考えられる。

試していないがConsul serverが1台だけならleader選定もへったくれもないのでこの問題が発生する余地はないだろう。

# Ansible playbookの修正

手作業でやっていれば自然に入っていたwaitが、Ansibleを使う場合のように処理の実行全体を自動化すると入らなかったのが原因、というわけで、アップグレードを行うAnsibleのplaybookにleader選定が完了するまで待つ処理を追加した。
秒数指定でも良かったのだけど5秒待ちでもアウトだったので余裕持たせると結構待たされるなあ、というのと秒数待ちだと簡単すぎてネタとして面白くなかったからやめた。

というわけで次が修正したConsulのダウンロード、展開、再起動を行うplaybookである。

```yaml:consul_install.yml
# assign the following variables with --extra_vars etc.
#  version: 0.5.2
#  local_dest: /opt
#  remote_dest: /usr/local/sbin
#  consul_addr: http://localhost:8500
#  consul_ui_unzip_dir: /var/lib/consul

- hosts: localhost
  tasks:
  - name: get official checksum
    set_fact: checksum={{ item | regex_replace(' +.+$', '') }}
    when: '"{{ version }}_linux_amd64.zip" in "{{ item }}"'
    with_url: https://dl.bintray.com/mitchellh/consul/{{ version }}_SHA256SUMS?direct
  - name: stat exec
    stat: path={{ local_dest }}/{{ version }}_linux_amd64.zip get_checksum=no get_md5=no
    register: exec
  - name: get local checksum
    set_fact: local_checksum={{ lookup('pipe', 'sha256sum ' + local_dest + '/' + version + '_linux_amd64.zip') | regex_replace(' +.+$', '') }}
    when: exec.stat.exists
  - name: download exec
    get_url:
      url: https://dl.bintray.com/mitchellh/consul/{{ version }}_linux_amd64.zip
      dest: '{{ local_dest }}'
      sha256sum: '{{ checksum }}'
    when: '"{{ checksum }}" != "{{ local_checksum | default() }}"'
  - name: stat ui
    stat: path={{ local_dest }}/{{ version }}_web_ui.zip get_checksum=no get_md5=no
    register: ui
  - name: download ui
    get_url:
      url: https://dl.bintray.com/mitchellh/consul/{{ version }}_web_ui.zip
      dest: '{{ local_dest }}'
    when: not ui.stat.exists

- hosts: all
  tasks:
  - name: unzip and copy exec
    unarchive: src={{ local_dest }}/{{ version }}_linux_amd64.zip dest={{ remote_dest }}

- hosts: consul-servers
  serial: 1
  tasks:
  - name: install python-httplib2
    yum: name=python-httplib2
    tags: upgrade
  - name: install ui
    unarchive: src={{ local_dest }}/{{ version }}_web_ui.zip dest={{ consul_ui_unzip_dir }}
  - name: restart consul
    service: name=consul state=restarted
  - name: check leader
    uri: url={{ consul_addr }}/v1/status/leader
    register: check
    until: "{{ check.status | default(500) }} == 200 and {{ check.json | default('') | length }} > 0"
    retries: 10
    delay: 2
    tags: upgrade

- hosts: all:!consul-servers
  tasks:
  - name: restart consul
    service: name=consul state=restarted
```

この件で追加したのはconsul-serversに対するplayの中のtags: upgradeになっている2つのtaskである。

* leader選定が終わっているかどうかは`http://localhost:8500/v1/status/leader`に聞くのが良さそうだったのでuriモジュールで問い合わせを行っている。
* uriモジュールの使用にはpython-httplib2をインストールする必要があるのでそのインストール。

使うにはplaybookの最初にコメントで書いたように-e(--extra_vars)で5つの変数を指定して実行する。

```
# ansible-playbook -i hosts consul_install.yml -e 'version=0.5.2 local_dest=/opt remote_dest=/usr/local/sbin consul_addr=http://localhost:8500 consul_ui_unzip_dir=/var/lib/consul'
```

まあversion以外はいつも一緒だろうからこのplaybookをincludeするだけのplaybookを作って、

```yaml:consul_install_include.yml
- include: consul_install.yml local_dest=/opt remote_dest=/usr/local/sbin consul_addr=http://localhost:8500 consul_ui_unzip_dir=/var/lib/consul
```

こう実行するなどした方がすっきりしてて良いだろう。

```
# ansible-playbook -i hosts consul_install_include.yml -e 'version=0.5.2'
```

ちなみに--skip-tags upgradeをオプションに指定すればアップグレードではなくインストールに使用可能である。

Consulのサービス化には[consulのinitスクリプトとsystemdユニットファイルを書いてみた](http://qiita.com/yunano/items/7ef5fa5670721de55627)を使用している。

# なお

冒頭の`Failed to create session: Unexpected response code: 500 (No cluster leader)`状態からのまともな復旧のさせ方はわからなかったので、停止してConsulのデータディレクトリを削除してもう一度起動した。
要は諦めて初期化したというわけである。