任意の値をFact Cachingにセットし、次回以降のplaybook実行時にその値を利用できるようにする

# はじめに

Ansible 1.8で追加された機能であるFact Cachingは、playbook実行時に行われるgather factsの際に集めたホストの情報を一定の期間保存し、他のホストでの別のplaybookの実行中に利用できるようにする。

私はFact Cachingという機能の概要を最初に聞いた際に、任意の値を記録できることを期待していた。
具体的には以下のような用途で利用したいと思っていた。

> 各ホストは適切な設定ファイルに共通のキーフレーズを指定する必要があるとする。最初のplaybook実行時にはキーフレーズをランダムに生成し、それを使用する。
> 後にシステムの負荷が高まったなどでホストが追加されたとする。同じplaybookを再度使用して追加したホストを構築する際、最初に生成したキーフレーズを取り出し、設定ファイルに指定する。

ところが最初に書いたように、Fact Cachingに保存されるのはホストの情報だけであった。"Fact" Cachingなので当たり前と言えば当たり前だ。

そこを何とかしてやろうとソースを読んだ結果、Fact Cachingに任意の値を書き込むことができたので以下に記述する。

# 事前準備

CentOSだと2014-12-12時点でepelのAnsibleはまだ1.7.2なのでepel-testingなどから1.8以上をインストールする。

```
# yum install epel-release
# yum install --enablerepo=epel-testing ansible
```

とりあえずここではFact Cachingの保存先にredisを使用することにする。
CentOS 7ではAnsibleの実行ホストにredisとpython-redisをインストールする。どちらもepelにある。
インストール後、redisを起動する。

```
# yum install redis python-redis
# systemctl start redis
```

CentOS 6ではpython-redisは古すぎるため、epelにあるものはFact Cachingに使用できない。pipでインストールするのが良いだろう。

```
# yum install redis python-pip
# pip install redis
# service redis start
```

ansible.cfgには以下を設定している。

```
[defaults]
fact_caching = redis
fact_caching_timeout = 315360000 # 10年くらい
```

# Fact Cachingにはどんな場合に値が書き込まれるのか

ソースより、おおよそ以下のようなことがわかった。
読んだソースは主に lib\ansible\playbook\\__init\_\_.py である。

* Ansibleが持つcache（playbook実行時の値の保管場所）にはsetup cacheとvars cacheという2つがある
 * ただし、利用者がアクセスできるのはsetup cacheとvars cacheを混ぜた後なので、値を利用するという意味では特に区別する必要はない
 * Fact Cachingはsetup cacheの保存に関する機能であり、vars cacheには影響しない。vars cacheは設定によらずメモリ上に保存され、playbook実行終了とともに消滅する
* モジュールが返した実行結果に"ansible_facts"というキーが含まれていた場合、"ansible_facts"の値がcacheに書き込まれる
 * ここで"ansible_facts"を返したモジュールがset_factかinclude_varsの場合はvars cacheに書き込まれる。他のモジュールだった場合はsetup cacheに書き込まれる
* gather fact実行時には内部でsetupモジュールが呼ばれている
 * setupモジュールは"ansible_facts"を返すset_fact、include_vars以外のモジュールなのでsetup cacheに書き込まれる
* registerを使った場合はvars cacheに書き込まれる

これらからわかることは、"ansible_facts"というキーを含む実行結果を返すモジュールを自分で作ればFact Caching(setup cache)に任意の値を保存させることができる、ということだ。

で、結局set_factモジュールをコピーして別の名前にすればいいだけじゃないか、と思いついた。

# action pluginについて

しかしset_factモジュールのソースである modules/core/utilities/logic/set_fact.py には処理の実体はない。
これはset_factモジュールの実体がaction pluginとして実装されているからである。

action pluginはモジュールとしてほぼ振る舞うが、通常のモジュールとの違いは、action pluginとして実装された処理はリモートホストではなくAnsibleを実行しているホストで実行されることである。

set_fact以外にaction pluginとして実体が実装されているモジュールの一例を挙げるとrawがある。操作対象にpythonがインストールされてなくてもrawモジュールが動く（かもしれない）のは、rawモジュールの実行主体がリモートホストではなくAnsibleを実行しているホストだからである。

set_factモジュールの実体が実装されたaction pluginは runner/action_plugins/set_fact.py がソースファイルである。
またaction pluginが実行されるためには、実体がなかったとしても同名のモジュールのソースファイルを配置する必要がある。これが modules/core/utilities/logic/set_fact.py である。

というわけでset_factをコピーしたset_cacheというモジュールを作ってやるには2ファイルをコピーしてやれば良い。
ここではコピーではなくシンボリックリンクを作ってやることにする。以下はCentOS 7での例。CentOS 6ならpython2.7のところがpython2.6になる。

```
# ln -s /usr/lib/python2.7/site-packages/ansible/modules/core/utilities/logic/set_{fact,cache}.py
# ln -s /usr/lib/python2.7/site-packages/ansible/runner/action_plugins/set_{fact,cache}.py
```

シンボリックリンクを作る場所は別のディレクトリでも良いが、その場合はansible.cfgの[defaults]セクションのaction_plugins項にaction pluginを置いたディレクトリを指定してやる。またモジュールを置いたディレクトリを指定する方法は複数あるが、ansible.cfgを使うなら[defaults]セクションのlibrary項に指定する。
ちなみに、ansible.cfgにはコメントアウトされたlibrary_path項が見えるがこれは違う、というかlibrary_pathなんて設定項目は存在しないはずだ。

# 簡単な実行例

以下のようなplaybookとinventoryを作成し、

```yaml:test.yml
- hosts: host1
  gather_facts: false
  tasks:
  - set_cache:
      aaa: 111
  tags: test1

- hosts: host2
  gather_facts: false
  tasks:
  - debug: var=hostvars['host1']['aaa']
  tags: test2
```

```txt:test.hosts
host1
host2
```

まず以下を実行してから、

```
# ansible-playbook test.yml -i test.hosts -t test1
```

以下を実行する。

```
# ansible-playbook test.yml -i test.hosts -t test2
```

実行結果に以下があれば上手く行っている。

```
ok: [host1] => {
    "hostvars['host1']['aaa']": "111"
}
```

# おわりに

これまでも値の保存を行いたければansible実行ホスト自身に対してtemplateモジュールなどでキーフレーズの入ったyamlファイルを作成して、後のplaybook実行ではinclude_varsモジュールなどでそのyamlファイルを読み込むようにする、などという手があったが、それよりはこちらの方がずっと簡単に利用できるだろう。