Ansible
AnsibleDay 22

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

追記: ここで紹介していることのほとんどがAnsible2系で解消しています。

この記事は古いバージョンであるAnsible1系時点での紹介であることをご注意ください。

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


  • できそうでできないので代替策を考えた

  • できなくてちょっとハマった

ことをまとめます。

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

ansible_logo_black_square.png

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

ansibleのversionは1.9.4です。


不便なこと


変数の使い回しとdict

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


group_vars/test-server.yml

app1_basedir: "/opt/app1"

app1_confdir: "{{ app1_basedir }}/conf"
app1_bindir: "{{ app1_basedir }}/bin"

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

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


group_vars/test-server.yml

app:

app1:
basedir: "/opt/app1"
confdir: "ここに何を書けばいい?"
bindir: "ここに何を書けばいい?"
app2:
basedir: "/opt/app2"
confdir: "ここに何を書けばいい?"
bindir: "ここに何を書けばいい?"

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


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"

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


tasks/main.yml

- name: debug messages app1_dict

debug: msg={{ item.value.confdir }}
with_dict: "{{ app }}"

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を動かす場合。

例えば、以下のような構成で、実際にディレクトリを作ってみます。


tasks/main.yml

- name: make directory

file: path={{ item.value.confdir }} state=directory
with_dict: "{{ app }}"


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"

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

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オプションで少しだけ詳細表示にすると、なんとか分かります。

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を書き間違えてしまいます。


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行目で:を入れ忘れています。

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

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ファイルを分けて運用したくなります。例えば、以下のような感じです。


tasks/main.yml

- include: app1.yml

- include: app2.yml

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


group_vars/test-server.yml

include_tasks:

- "app1.yml"
- "app2.yml"


tasks/main.yml

- include: "{{ item }}"

with_items: "{{ include_tasks }}"

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

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ファイルなどに有効/無効スイッチを書いて、それで判別するしかなさそうです。

[test-server]

192.168.33.11 app1=True app2=False


tasks/main.yml

- include: app1.yml

when: app1 == True

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


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_*さんが仲直りしていたので幸せが訪れた。

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


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

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

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

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


handlerを即動かす方法がない

例えば、


  • configに差分があったらファイルを入れ替える

  • configファイルを入れ替えた場合はミドルウェア再起動

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

Chef: notifies syntax

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

Ansible: playbooks_intro

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

Ansible: notify と handlers の使い方について調べた | CUBE SUGAR CONTAINER


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

role間に依存関係がある場合に、metaにdependenciesを書くケースがあります。

例えば、role-elasticsearchrole-jdkに依存している、など。


role-test2/meta/main.yml

dependencies:

- { role: 'role-test' }

上記の例では、role-test2role-testと依存関係を持っている事になります。

この場合、playbookファイルは、依存関係を勝手に解消してくれます。


test-server.yml

- hosts: test-server

sudo: yes
roles:
# - role-test ← これは不要
- role-test2

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

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

Advanced Control over Role Requirements Files | ansible-galaxy


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にもそんな機能があればいいなと思います。

  berks viz                   # Visualize the dependency graph


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

Ansible: file_module

上記の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.


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


tasks/main.yml

- name: make directory

file: path={{ app1_confdir }} state=directory mode=0644

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

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

今さら聞きづらい「ファイルパーミッション」について | fenrir Developer's Blog

具体的には、serverspecに直接同じ値(例えば0644とか)を利用してテストしようとすると、エラーになります。

ですので、もしテストで同じ変数を使う場合などは、0+{{ permission }}みたいなことをする必要があります。(どちらかというとserverspec側の問題のような気もしますが)

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


最後に

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