ここがヘンだよAnsible (ハマりどころと対策について)

**追記: ここで紹介していることのほとんどがAnsible2系で解消しています。**
**この記事は古いバージョンであるAnsible1系時点での紹介であることをご注意ください。**

タイトルは煽っていますが、Ansibleを使っていく中で

* できそうでできないので代替策を考えた
* できなくてちょっとハマった

ことをまとめます。
だからといって「Ansibleはクソだ」と言うつもりは全く無く、むしろ **不便さを補って余りあるほどメリットがある**ので、今後もAnsibleを使い続けると思います。

<img width="200px" alt="ansible_logo_black_square.png" src="https://qiita-image-store.s3.amazonaws.com/0/16458/21608a05-49bf-5782-8c3c-482788e018ea.png">

自分が知らないだけかもなので、こんな風に解決してるよって話があればツッコミ大歓迎です。
ansibleのversionは1.9.4です。

# 不便なこと

## 変数の使い回しとdict

例えば、アプリケーションのホームディレクトリを指定しておいて、configディレクトリはホームディレクトリ配下のconf, binディレクトリはホームディレクトリ配下のbinとするときには、varsは以下のような書き方になると思います。

```yaml:group_vars/test-server.yml
app1_basedir: "/opt/app1"
app1_confdir: "{{ app1_basedir }}/conf"
app1_bindir: "{{ app1_basedir }}/bin"
```

この場合は、`app1_basedir`だけを変更すれば、その配下のディレクトリも全部参照してくれるので、一気に変更することができて便利です。

なら、アプリケーションが複数(例えば複数インスタンスとか)起動する場合に、ちょっとまとめてしまいたいとするとどうするか。

```yaml:group_vars/test-server.yml
app:
  app1:
    basedir: "/opt/app1"
    confdir: "ここに何を書けばいい？"
    bindir: "ここに何を書けばいい？"
  app2:
    basedir: "/opt/app2"
    confdir: "ここに何を書けばいい？"
    bindir: "ここに何を書けばいい？"
```

ここで、ついうっかり以下のような書き方をしても、varsのconversionが通りません。

```yaml:group_vars/test-server.yml
app:
  app1:
    basedir: "/opt/app1"
    confdir: "{{ app.app1.basedir }}/conf"
    bindir: "{{ app.app1.basedir }}/bin"
  app2:
    basedir: "/opt/app2"
    confdir: "{{ app.app2.basedir }}/conf"
    bindir: "{{ app.app2.basedir }}/bin"
```

以下のような感じで、無限ループで止まります。

```yaml:tasks/main.yml
- name: debug messages app1_dict
  debug: msg={{ item.value.confdir }}
  with_dict: "{{ app }}"
```


```ansible-playbook実行時
TASK: [role-test | debug messages app1_dict] **********************************
fatal: [192.168.33.11] => Failed to template msg={{ app.app1.basedir }}: Failed to template {{ app.app1.basedir }}/conf: Failed to template {{ app.app1.basedir }}/conf: Failed to template {{ app.app1.basedir }}/conf: Failed to template {{ app.app1.basedir }}/conf: Failed to template {{ app.app1.basedir }}/conf: Failed to template {{ app.app1.basedir }}/conf: Failed to template {{ app.app1.basedir }}/conf: Failed to template {{ app.app1.basedir }}/conf: Failed to template {{ app.app1.basedir }}/conf: Failed to template (略): Failed to template {{ a
```

解決策としては、以下が考えられます。

1. `basedir`を別のwithループに入れる  
結局同じwithの中で賄ってしまおうとしているからこうなってしまうので、別のループで管理してしまいます。ただ、非常に美しくない…。というか見難い。
2. 変数の使い回しをやめる  
こっちも美しくないですが、`/opt/app1` を全て直書きしてしまう。上記程度であればそれほど苦労しないだろうという逃げです。
3. withループをやめてしまう  
ネストする形式をやめる案です。今まで通り変数の使い回しができますが、上記の例でいうアプリケーションの数が変動するようなケースに弱いです。

私を2番目を選択していますが、もっといい方法がないものかと思っています。

## 結果が見づらい

上記のようなdict形式のものでmoduleを動かす場合。
例えば、以下のような構成で、実際にディレクトリを作ってみます。

```yaml:tasks/main.yml
- name: make directory
  file: path={{ item.value.confdir }} state=directory
  with_dict: "{{ app }}"
```

```yaml:group_vars/test-server.yml
app:
  app1:
    basedir: "/opt/app1"
    confdir: "/opt/app1/conf"
    bindir: "/opt/app1/bin"
  app2:
    basedir: "/opt/app2"
    confdir: "/opt/app2/conf"
    bindir: "/opt/app2/bin"
```    

実行結果は以下になります。

```ansible-playbook実行時
TASK: [role-test | make directory] ********************************************
changed: [192.168.33.11] => (item={'key': 'app2', 'value': {'confdir': '/opt/app2/conf', 'basedir': '/opt/app2', 'bindir': '/opt/app2/bin'}})
changed: [192.168.33.11] => (item={'key': 'app1', 'value': {'confdir': '/opt/app1/conf', 'basedir': '/opt/app1', 'bindir': '/opt/app1/bin'}})
```

これで何やってるか分かるでしょうか。自分には全く分かりません…。
実際には`confdir`を作ってるだけなのですが、varsが全て表示されるため、何がなんだか分からないという状態です。

ちなみに、`-v`オプションで少しだけ詳細表示にすると、なんとか分かります。

```ansible-playbookで-vオプション追加時
TASK: [role-test | make directory] ********************************************
changed: [192.168.33.11] => (item={'key': 'app2', 'value': {'confdir': '/opt/app2/conf', 'basedir': '/opt/app2', 'bindir': '/opt/app2/bin'}}) => {"changed": true, "gid": 0, "group": "root", "item": {"key": "app2", "value": {"basedir": "/opt/app2", "bindir": "/opt/app2/bin", "confdir": "/opt/app2/conf"}}, "mode": "0755", "owner": "root", "path": "/opt/app2/conf", "size": 4096, "state": "directory", "uid": 0}
changed: [192.168.33.11] => (item={'key': 'app1', 'value': {'confdir': '/opt/app1/conf', 'basedir': '/opt/app1', 'bindir': '/opt/app1/bin'}}) => {"changed": true, "gid": 0, "group": "root", "item": {"key": "app1", "value": {"basedir": "/opt/app1", "bindir": "/opt/app1/bin", "confdir": "/opt/app1/conf"}}, "mode": "0755", "owner": "root", "path": "/opt/app1/conf", "size": 4096, "state": "directory", "uid": 0}
```

おっ、どうやら`path`のディレクトリを作っているようだ。…分かります？
ansibleは、この`with_dict`形式の表示がすこぶる不親切なイメージがあります。(そうでなくても若干分かりづらいと思うのは自分だけ？)
特にChefを使ってた人間からすると、すごく見づらいと思っています…。

## syntaxエラーが分かりづらい

例えば、tasksを書き間違えてしまいます。

```yaml:tasks/main.yml
---
# tasks file for role-test
- name: debug messages app1 confdir
  debug: msg={{ app1_confdir }}

- name: make directory
  file path={{ app1_confdir }} state=directory

- name: debug messages app1 bindir
  debug: msg={{ app1_bindir }}
```

7行目で`:`を入れ忘れています。

実行時は、こんなふうに怒られます。

```ansible-playbook実行時
ERROR: Syntax Error while loading YAML script, /vagrant/ansible-playbook/roles/role-test/tasks/main.yml
Note: The error may actually appear before this position: line 9, column 1


- name: debug messages app1 bindir
^
```

…どこ？
ただ、これはある程度はやむを得ない話かなぁと思います…。

## includeとwithが併用できない

taskを全て`main.yml`に書いてしまうと利便性が低くなるので、ある程度taskファイルを分けて運用したくなります。例えば、以下のような感じです。

```yaml:tasks/main.yml
- include: app1.yml
- include: app2.yml
```

この場合に、状況によって読み込むincludeファイルを変えるために、`vars`にincludeするymlファイルを書きたくなります。

```yaml:group_vars/test-server.yml
include_tasks:
  - "app1.yml"
  - "app2.yml"
```

```yaml:tasks/main.yml
- include: "{{ item }}"
  with_items: "{{ include_tasks }}"
```

が、そんなことはできません。

```ansible-playbook実行時
ERROR: [DEPRECATED]: include + with_items is a removed deprecated feature (in /vagrant/ansible-playbook/roles/role-test/tasks/main.yml).  Please update your playbooks.
```

現時点では、inventoryファイルなどに有効/無効スイッチを書いて、それで判別するしかなさそうです。

```inventory/test-server
[test-server]
192.168.33.11 app1=True app2=False
```

```yaml:tasks/main.yml
- include: app1.yml
  when: app1 == True

- include: app2.yml
  when: app2 == True
```

```ansible-playbook実行時
TASK: [role-test | debug messages app1] ***************************************
ok: [192.168.33.11] => {
    "msg": "this is app1"
}

TASK: [role-test | debug messages app2] ***************************************
skipping: [192.168.33.11]
```

この例では、inventoryに対象サーバでapp1,app2を有効/無効として変数を設定し、その変数に従ってincludeするymlをtaskで切り替えています。

ただ、これはansible2.0で解消する見込みです。
これは Ansible Advent Calendar 2015 19日目の @t_nakayama0714 さんの以下の記事が詳しいです。

[includeさんとwith_*さんが仲直りしていたので幸せが訪れた。](http://qiita.com/t_nakayama0714/items/4bcca0dd70a228c5b73b)

自分もAnsible2.0を心待ちにしています！

## ansibleコマンドでvarsを取り込む方法がない

`ansible-playbook`コマンドだと`group_vars`下の変数などを読み込む方法はありますが、単なる`ansible`コマンドだとそんな大層なことはできません。

ansibleで運用作業アプリケーションのデプロイとかミドルウェアの再起動を行いたいときに、使いたい変数をそのまま流用できないのが辛い。inventoryは持ってるのに、変数が使えない…。

運用作業が型化されていれば、もうansibleではなくansible-playbookで(=つまりplaybookを書いてしまう)実現するほうが良さそうです。あるいは、そもそもansibleに任せない。

## handlerを即動かす方法がない

例えば、

* configに差分があったらファイルを入れ替える
* configファイルを入れ替えた場合はミドルウェア再起動

という操作を行いたい場合、一連のタスクの中でミドルウェア再起動を「その時点ですぐに」行いたい場合があります。例えば、ミドルウェアを再起動したあとに後続のタスクを動かしたい場合などです。Chefでは、handlerに相当する`notifies`に`immediately`のオプションがありますが、Ansibleにはありません。

[Chef: notifies syntax](https://docs.chef.io/resource_common.html#notifies-syntax)

近いものに、 `- meta: flush_handlers`という書き方がありますが、これはhandlerを全てその場で発火させるように見えるので、目的とは違うでしょう。

[Ansible: playbooks_intro](http://docs.ansible.com/ansible/playbooks_intro.html)

workaround的には、`register`〜`when`を使えということなんでしょうけれども、それってhandlerの範疇なんじゃないの…？と思ったりもします。

[Ansible: notify と handlers の使い方について調べた | CUBE SUGAR CONTAINER](http://blog.amedama.jp/entry/2015/09/01/214912)


## role間の依存関係を把握しづらい

role間に依存関係がある場合に、metaにdependenciesを書くケースがあります。
例えば、`role-elasticsearch`は`role-jdk`に依存している、など。

```yaml:role-test2/meta/main.yml
dependencies:
  - { role: 'role-test' }
```

上記の例では、`role-test2`は`role-test`と依存関係を持っている事になります。
この場合、playbookファイルは、依存関係を勝手に解消してくれます。

```test-server.yml
- hosts: test-server
  sudo: yes
  roles:
#    - role-test ← これは不要
    - role-test2
```

```ansible-playbook実行時
TASK: [role-test | debug messages role-test] **********************************
ok: [192.168.33.11] => {
    "msg": "this is role-test"
}

TASK: [role-test2 | debug messages role-test2] ********************************
ok: [192.168.33.11] => {
    "msg": "this is role-test2"
}
```

これ自体は嬉しい挙動かなと思います。
ちなみに、上記の「これは不要」をコメントアウトしなかったら、思いっきり二重で動きます。

```
TASK: [role-test | debug messages role-test] **********************************
ok: [192.168.33.11] => {


- hosts: test-server
    "msg": "this is role-test"
}

TASK: [role-test | debug messages role-test] **********************************
ok: [192.168.33.11] => {
    "msg": "this is role-test"
}

TASK: [role-test2 | debug messages role-test2] ********************************
ok: [192.168.33.11] => {
    "msg": "this is role-test2"
}
```

勝手にまとめてくれると嬉しいのですが、望み過ぎですかね…。

一方で、ansible-galaxy。

[ansible galaxy](https://galaxy.ansible.com/)

必要なroleを列挙さえすれば、そのroleを勝手にインストールしてくれるというスグレモノなのですが、role間の依存関係がクセモノで、どうやら公式ansible-galaxy上にリポジトリを登録した場合のみ、自動的に依存関係を解消して必要なroleを取得してくれるようです。

[Advanced Control over Role Requirements Files | ansible-galaxy](http://docs.ansible.com/ansible/galaxy.html#advanced-control-over-role-requirements-files)

> To download a role with dependencies, and automatically install those dependencies, the role must be uploaded to the Ansible Galaxy website.

現在、私のところではgitのプライベートリポジトリを立てて、そこでansible roleを管理しているのですが、

* ansible-galaxyでプライベートリポジトリからroleをインストールする : 依存関係も含めて全部requirementsに記載する必要がある
* ansible-playbookで実際にプロビジョニングする : playbookにはdependencyの親だけ記載すればよい

という感じでアンマッチなのが気持ち悪いです。ansible-galaxyの公式リポジトリ使えってことなんでしょうか。

ということなので、少なくとも私の環境では結局dependencyを理解してansible-galaxyを実行する必要があり、片手落ちな感じが否めません。

ところで、Chefだとberkshelfが `berks vis`というコマンドを提供しており、それによってcookbookの依存関係をグラフ化してくれるのですが、ansible-galaxyにもそんな機能があればいいなと思います。

```helpより
  berks viz                   # Visualize the dependency graph
```

## fileモジュールなどで指定するpermissionの先頭は(4桁表示の場合は)必ず0である必要がある

[Ansible: file_module](https://docs.ansible.com/ansible/file_module.html)

上記の`mode`オプションには、以下のようにあります。

> For those used to /usr/bin/chmod remember that modes are actually octal numbers (like 0644). Leaving off the leading zero will likely have unexpected results.

例えば、以下のように記述する必要があるということです。

```yaml:tasks/main.yml
- name: make directory
  file: path={{ app1_confdir }} state=directory mode=0644
```

先頭に`0`を入れなさい、と。

普通、パーミッションの4桁目はsticky bitなどの特別な意味を持ちます。

[今さら聞きづらい「ファイルパーミッション」について | fenrir Developer's Blog](http://blog.fenrir-inc.com/jp/2012/02/file_permission.html)

具体的には、`serverspec`に直接同じ値(例えば`0644`とか)を利用してテストしようとすると、エラーになります。
ですので、もしテストで同じ変数を使う場合などは、`0+{{ permission }}`みたいなことをする必要があります。(どちらかというとserverspec側の問題のような気もしますが)

ansible的には、8進数を判断するために先頭に0を付与しているので、`0+3桁`、`0+4桁`、`4桁`のいずれかでもよさそうです。(コメントありがとうございます)

# 最後に

しつこいですが、それでもansibleには非常に多くのメリットがあります。私はこれからもansibleを使い続けます。いつかメリットも書きたいです。