はじめに

Ansible Vaultはとても遅い。
インベントリにたくさんのホストが書かれていて、かつhost_vars以下にそのたくさんのホストの分のAnsible Vaultで暗号化されたファイルが置かれている状況だとそれが良く分かる。

実際にどれだけ遅いか見てみよう。
以下、VirtualBox上に立てた1CPU, 1GBの仮想マシンのCentOS 7.1にAnsible 1.9.2をインストールして確認している。grep aes /proc/cpuinfoで見る限りAES-NIは利用できるが、どれくらい意義があるのかは調べていない。

host_varsディレクトリにhost00.ymlからhost99.ymlまでの計100個、Ansible Vaultで暗号化されたファイルを作る。

$ mkdir host_vars
$ ansible-vault create host_vars/host00.yml # 中身を「key1: val1」にする
$ for n in {01..99}; do cp host_vars/host00.yml host_vars/host${n}.yml; done
$ vi vault # ansible-vault実行時に設定したパスワードを記述する

インベントリファイルを以下の内容で作成する。つまりhost00からhost99まで100個のホストを記述したことになる。

hosts
[test]
host[00:99]

playbookを以下の内容で作成する。

read1.yml
- hosts: host00
  connection: local
  gather_facts: no
  tasks:
  - debug: var=key1

見ての通り、対象としているホストはインベントリファイルに書いた全部ではなくhost00だけである。
なお、debugモジュールはlocalhostで実行されるので「connection: local」はわかりやすいように書いているだけで特に意味はない。何にせよインベントリに記述したホストは実際に存在していなくても良い。

さて、実行してみよう。

$ time ansible-playbook -i hosts --vault-password-file vault read1.yml

PLAY [host00] *****************************************************************

TASK: [debug var=key1] ********************************************************
ok: [host00] => {
    "var": {
        "key1": "val1"
    }
}

PLAY RECAP ********************************************************************
host00                     : ok=1    changed=0    unreachable=0    failed=0


real    1m30.620s
user    1m30.475s
sys     0m0.052s

たったこれだけで90秒も掛かるのである。

インベントリを以下のようにhost00だけにしたとすると、1秒ちょっとで終了する。

hosts.1
[test]
host00
$ time ansible-playbook -i hosts.1 --vault-password-file vault read1.yml

real    0m1.314s
user    0m1.150s
sys     0m0.030s

つまり対象のホストが1つしかなかったとしても、インベントリにあるだけhost_vars以下のファイルを全部開く、という動きをするようだ。

それではどうすれば良いか

というわけでまず、1つのホスト分しかAnsible Vaultから取得しないで良い場合にどのように高速化するか。
playbookごとにインベントリファイルを別々にして、インベントリファイルに載せるホスト数を減らすのも1つの手かもしれないが、その後の手間の点でちょっといまいちである。
ここは頑張ってplaybook内で1つのホスト分しか取得しないようにしてみる。

まず、先に作ったhost_varsをリネームして、Ansible Vaultで作ったファイルはhost_varsに置かないようにする。

$ mv host_vars/ _host_vars

lookupプラグインの1つ、pipeを使ってansible-vault viewを実行し、必要なホストの分だけ取り出してset_factモジュール経由で渡すようにする。

read1a.yml
- hosts: host00
  connection: local
  gather_facts: no
  tasks:
  - set_fact: "key1={{ lookup('pipe', 'ansible-vault view _host_vars/' + inventory_hostname + '.yml --vault-password-file vault | grep "key1: " | sed "s/key1: //"') }}"
  - debug: var=key1

このplaybookを実行してみる。

$ time ansible-playbook -i hosts read1a.yml

PLAY [host00] *****************************************************************

TASK: [set_fact key1={{ lookup('pipe', 'ansible-vault view _host_vars/' + inventory_hostname + '.yml --vault-password-file vault | grep "key1: " | sed "s/key1: //"') }}] ***
ok: [host00]

TASK: [debug var=key1] ********************************************************
ok: [host00] => {
    "var": {
        "key1": "val1"
    }
}

PLAY RECAP ********************************************************************
host00                     : ok=2    changed=0    unreachable=0    failed=0


real    0m0.819s
user    0m0.732s
sys     0m0.066s

想定通り速くなった。

ちなみに、ansible_ssh_passに値をセットすれば期待通りにSSHのパスワードログインもできるようになる。
ただし、gather_factsが動くよりも先にセットしないとならないため、playbookは以下のような感じになるだろう。

- hosts: host00
  gather_facts: no
  tasks:
  - set_fact: "ansible_ssh_pass={{ lookup('pipe', 'ansible-vault view _host_vars/' + inventory_hostname + '.yml --vault-password-file vault | grep "ansible_ssh_pass: " | sed "s/ansible_ssh_pass: //"') }}"

- hosts: host00
  gather_facts: yes
  tasks:
  - file: dest=/tmp/1 state=touch

そもそもAnsible Vaultなど要らないのでは

では、1つのホスト分だけでなく100個のホスト分全部必要な場合はどうだろうか。

read100a.yml
- hosts: test
  gather_facts: no
  serial: 1
  tasks:
  - set_fact: "key1={{ lookup('pipe', 'ansible-vault view _host_vars/' + inventory_hostname + '.yml --vault-password-file vault | grep "key1: " | sed "s/key1: //"') }}"

- hosts: test
  gather_facts: no
  tasks:
  - debug: var=key1

「serial: 1」にしているのは増やしても速くならなかった上に増やし過ぎると読み込みに失敗してエラーになったからである。

実行してみる。

$ time ansible-playbook -i hosts read100a.yml -f 100

real    1m5.342s
user    0m58.082s
sys     0m4.108s

当然時間が掛かる。
しかしどうせコマンド叩いて値を取得してるのだったらAnsible Vaultなんて使わずに別のもっと速そうなの使えば良いのではなかろうか、というわけでopenssl encを使ってみる。一応256bitsのAESを使ってAnsible Vaultと同条件にする。

$ mkdir openssl
$ echo "key1: val1" | openssl enc -aes256 -e -kfile vault -out openssl/host00
$ for n in {01..99}; do cp openssl/host00 openssl/host${n}; done

playbookの変更はset_factの箇所だけ。

read100b.yml
- hosts: test
  gather_facts: no
  serial: 1
  tasks:
  - set_fact: "key1={{ lookup('pipe', 'openssl enc -aes256 -d -kfile vault -in openssl/' + inventory_hostname + ' | grep "key1: " | sed "s/key1: //"') }}"

- hosts: test
  gather_facts: no
  tasks:
  - debug: var=key1

実行してみる。

$ time ansible-playbook -i hosts read100b.yml -f 100

real    0m5.489s
user    0m1.956s
sys     0m1.427s

十分である。インベントリのホスト数が多い場合はAnsible Vaultなんて使わずにもうこれで良いのではないだろうか。
まあ安全性を高める目的でopenssl encの復号パスワードの取得のためにAnsible Vaultを使っても良いかもしれない。それだとAnsible Vaultが使われるのは1回だけなのでパフォーマンス上の影響はほとんどなく、playbook実行時にパスワードを入力して実行させられる。
この場合、openssl encでは-kfileオプションの代わりに-kオプションなどでパスワードをセットすることになる。

playbookで他のホストの変数を取得したい場合はhostvarsが使用できる。
debug: var=hostvars['host99'].key1などとすれば良い。

ちなみにhost_vars以下にAnsible Vaultで作ったファイルを戻して以下のread100.yml(普通に100個のホスト分読むもの)を実行したら98秒だった。

read100.yml
- hosts: test
  connection: local
  gather_facts: no
  tasks:
  - debug: var=key1

参考:他のバージョンのAnsibleでAnsible Vaultを使う

開発版を使ってみる

開発版だとこんなことをしなくてもAnsible Vaultが普通に使えたりはしないだろうか。

$ sudo yum install git gcc python-devel python-pip
$ sudo pip install paramiko PyYAML Jinja2 httplib2 six
$ git clone git://github.com/ansible/ansible.git --recursive
$ cd ansible/
$ source hacking/env-setup
$ ansible-playbook --version
ansible-playbook 2.0.0 (devel 6154ed1dda) last updated 2015/08/30 20:17:52 (GMT +900)
  lib/ansible/modules/core: (detached HEAD 66288d48a2) last updated 2015/08/30 20:18:02 (GMT +900)
  lib/ansible/modules/extras: (detached HEAD 8d8f1c510d) last updated 2015/08/30 20:18:16 (GMT +900)
  config file =
  configured module search path = None

最初に作ったホスト1つだけ取得するplaybook(read1.yml)を実行してみる。

$ time ansible-playbook -i hosts --vault-password-file vault read1.yml

real    0m47.208s
user    0m46.656s
sys     0m0.180s

実行時間が半分くらいになったが、まだ遅い。
しかしpipでcryptographyをインストールしておくともっと速くなるらしい。

$ sudo yum install libffi-devel openssl-devel
$ sudo pip install cryptography

read1.ymlを実行。

$ time ansible-playbook -i hosts --vault-password-file vault read1.yml

real    0m7.779s
user    0m7.022s
sys     0m0.156s

まあ悪くはない。

ホスト100個とも取得するplaybook(read100.yml)はどうだろうか。

$ time ansible-playbook -i hosts --vault-password-file vault read100.yml -f 10

real    0m31.422s
user    0m27.608s
sys     0m0.660s

これ、時間が掛かっているのはAnsible Vaultが原因ではなくてdebugモジュールの出力に時間が掛かるせい。
「-f 100」にしたら出力できなくてエラーになったし。
debugモジュールの箇所をコメントアウトすると7秒程度になる。

ついでにpipeプラグインもエラーになって動かなかった。

Ansible 1.6.10

実はインベントリに定義したホスト全部読むようになったのは1.7からなのだ。

$ sudo yum install gcc python-devel python-pip
$ sudo pip install ansible==1.6.10

read1.ymlは当然速い。

$ time ansible-playbook -i hosts --vault-password-file vault read1.yml

real    0m1.008s
user    0m0.669s
sys     0m0.091s

read100.ymlは時間が掛かるが1.9.2よりは速い。

$ time ansible-playbook -i hosts --vault-password-file vault read100.yml -f 100

real    0m48.760s
user    0m46.572s
sys     0m0.753s
TOP