Ansible の shell や command モジュールで冪等性を保つ方法

kamoAnsible,command,shell,冪等性

AnsibleLogo_transparent_web

はじめまして、入社1年と数ヶ月のkamoです。初投稿になります。

最近、巷で噂のRaspberry Pi B+を買ってしまいました。

え?この記事を書かずにそんなことしてん?何してんの? 他にもやることあるよね、kamo?
という心の声が聞こえてきましたが、学生の頃、テスト期間中に勉強しなければいけないのに、机の上や部屋の掃除をしてしまう感じで買ってしまいました。すみません。
Raspberry Pi B+で何をするかは、まだ考えていません。色々と片付いたら弄ろうと思っていますので、ご勘弁を。

さて、最近はサーバを構成管理ツールで管理するのが流行っていますが、トライコーンでは Ansible を使っての構成管理を試してみています。

その中でハマったのが shellcommand モジュールを使いながら冪等性を保つ方法です。VagrantとAnsibleで開発環境を構築 – テストとハマったこと でも紹介されていますが、これらのモジュールは普通に使っていると冪等性を保った挙動をしてくれません。

もちろん用意されているモジュールで事足りれば良いのですが、そうでない場合も多々あります。

例えば、Linux で Windows とファイル共有などする場合に重宝する samba を構築する場合を考えてみます。

ざっくり Playbook は以下のようになります。(smb.conf の変更なども実際には必要ですが割愛しています)。shell モジュールはユーザの追加の際に必要になります。コマンド自体は pdbedit を用い、-t オプションを使うことでパスワードを標準入力から読み込むようにしてワンライナーで書いています。

- name: be sure samba is installed
  yum: pkg=samba4,samba4-common,samba4-client state=latest

- name: Linux User Setting for samba
  user: name=smbuser

- name: Add samba user
  shell: echo -e "passwd\npasswd\n" | pdbedit -t -a smbuser

ではこの Playbook を用いて ansible-playbook を実行してみましょう。

$ ansible-playbook -v -i inventory site.yml
PLAY [servers] ************************************************************

GATHERING FACTS ***************************************************************
ok: [samba-server]

TASK: [servers | be sure samba is installed] ******************************
changed: [samba-server] => {"changed": true, "item": "",
      "msg": "", "rc": 0, "results": ["Loaded plugins: fastestmirror,
      presto\nLoading mirror speeds from cached ...
      \n\nComplete!\n"]}

TASK: [servers | Linux User Setting for samba] ****************************
changed: [samba-server] => {"changed": true, "comment": "",
      "createhome": true, "group": 1000, "home": "/home/smbuser", "item": "",
      "name": "smbuser", "shell": "/bin/bash", "state": "present", "stderr": ...
      "system": false, "uid": 1000}

TASK: [Add samba user] ****************************************
changed: [samba-server] => {"changed": true,
      "cmd": "echo -e \"passwd\\npasswd\\n\" | pdbedit -t -a smbuser ",
      "delta": "0:00:00.046766", "end": "2014-10-10 17:24:40.143402", "item": "",
      "rc": 0, "start": "2014-10-10 17:24:40.096636", "stderr": "",
      "stdout": ... }

PLAY RECAP ********************************************************************
samba-server: ok=0
changed=3    unreachable=0    failed=0

無事に samba を利用するためのパッケージのインストールと samba でのアクセス用ユーザ smbuser が作成されたようです。確認してみましょう。

[root@samba-server ~]# rpm -qa | grep samba
samba4-common
samba4-libs
samba4
samba4

[root@samba-server ~]# pdbedit -L
smbuser:1000:

インストールとユーザの作成、無事完了していますね。

さて次に、ansible-playbook コマンドを再度実行してみましょう。
本来 Ansible に期待するのは、冪等性が保たれて何も実行されない、つまり ok=3, change=0 となることです。

$ ansible-playbook -v -i inventory site.yml
PLAY [servers] ************************************************************

GATHERING FACTS ***************************************************************
ok: [samba-server]

TASK: [servers | be sure samba is installed] ******************************
ok: [samba-server] => {"changed": false, "item": "", "msg": "",
      "rc": 0, "results": ["All packages providing samba4 are up to date",
      "All packages providing samba4-common are up to date",
      "All packages providing samba4-client are up to date"]}

TASK: [servers | Linux User Setting for samba] ****************************
ok: [samba-server] => {"append": false, "changed": false, "comment": "",
      "group": 1000, "home": "/home/smbuser", "item": "", "move_home": false, 
      "name": "smbuser", "shell": "/bin/bash", "state": "present", "uid": 1000}

TASK: [servers | Add samba user] ****************************************
changed: [samba-server] => {"changed": true,
      "cmd": "echo -e \"passwd\\npasswd\\n\" | pdbedit -t -a smbuser ",
      "delta": "0:00:00.046766", "end": "2014-10-10 17:24:40.143402",
      "item": "", "rc": 0, "start": "2014-10-10 17:24:40.096636",
      "stderr": "", "stdout": ... }

PLAY RECAP ********************************************************************
samba-server: ok=2
changed=1    unreachable=0    failed=0

おや、おかしいですね。changed=1 が表示されてしまいました。まあ当然は当然で、shell や command モジュールは検査の機構が無く、現状と結果の差を確認せずにコマンドを実行してしまうためです。

pdbedit での同名のユーザを追加しているだけなので実質的には実行前後で同じではあるのですが、本来調べればわかるのに pdbedit で実際にユーザ追加のコマンド叩いちゃってますし、changed=1 って表示もされちゃっているので、Ansible で期待される冪等性は保てていないと言って良いでしょう。

この Playbook を単体で使っているだけであれば「ああ分かってることだし」とスルーもできますが、多数の Playbook からなるインストール処理を行っている際、本来 changed=0 が期待される所、shell や command モジュールを使った箇所の数だけ changed が出てしまうのはやっぱり微妙ですね。

要するに条件判定を自作すればいいわけなのですが、以下のようなタスクを追加すればそれが実現できます。

- name: Samba User List
  shell: pdbedit -L | awk -F":" '{print $1}'
  register: samba_user_list
  changed_when: false
  always_run: yes

実行すると以下のような結果が得られます。

TASK: [servers | Samba User List] *****************************************
ok: [samba-server] => {"changed": false,
      "cmd": "pdbedit -L | awk -F\":\" '{print $1}' ", 
      "delta": "0:00:00.021991", "end": "2014-10-10 17:24:39.250869", 
      "item": "", "rc": 0, "start": "2014-10-10 17:24:39.228878", 
      "stderr": "", "stdout": "smbuser", "stdout_lines":["smbuser"]}

“always_run: yes" のため常に実行され、"changed_when: false" のため常に ok と表示されるようになっています。"register: samba_user_list" によって、pdbedit -L | awk -F":" '{print $1}'の結果が、samba_user_list.stdout (テキスト形式) および samba_user_list.stdout_lines (配列形式) に格納されます。

これを利用して Add samba user タスクを書き直してみましょう。

- name: be sure samba is installed
  yum: pkg=samba4,samba4-common,samba4-client state=latest

- name: Linux User Setting for samba
  user: name=smbuser

- name: Samba User List
  shell: pdbedit -L | awk -F":" '{print $1}'
  register: samba_user_list
  changed_when: false
  always_run: yes

- name: Add samba user
  shell: echo -e "passwd\npasswd\n" | pdbedit -t -a smbuser
  when: samba_user_list.stdout_lines.count("smbuser") < 1

最後の行に、配列 samba_user_list.stdout_lines の中から “smbuser" の数を数え、これが 1 以下 (要するに見つからなかった場合) に実行するよう条件分岐が追加されました。

では再度 ansible-playbook を実行してみましょう。

$ ansible-playbook -v -i inventory site.yml
PLAY [servers] ************************************************************

GATHERING FACTS ***************************************************************
ok: [samba-server]

TASK: [servers | be sure samba is installed] ******************************
ok: [samba-server] => {"changed": false, "item": "", "msg": "",
      "rc": 0, "results": ["All packages providing samba4 are up to date",
      "All packages providing samba4-common are up to date",
      "All packages providing samba4-client are up to date"]}

TASK: [servers | Linux User Setting for samba] ****************************
ok: [samba-server] => {"append": false, "changed": false, "comment": "",
      "group": 1000, "home": "/home/smbuser", "item": "", "move_home": false, 
      "name": "smbuser", "shell": "/bin/bash", "state": "present", "uid": 1000}

TASK: [servers | Samba User List] *****************************************
ok: [samba-server] => {"changed": false,
      "cmd": "pdbedit -L | awk -F\":\" '{print $1}' ", 
      "delta": "0:00:00.021991", "end": "2014-10-10 17:24:39.250869", 
      "item": "", "rc": 0, "start": "2014-10-10 17:24:39.228878", 
      "stderr": "", "stdout": "smbuser", "stdout_lines":["smbuser"]}

TASK: [servers | Add samba user] ****************************************
skipping: [samba-server]

PLAY RECAP ********************************************************************
samba-server: ok=3    changed=0    unreachable=0    failed=0

Add samba user のタスクが skipping となっている事がわかります。残念ながら skipping のタスクは ok のカウントには入りませんが、changed=0 にはなっていることが分かりますね。また不要な pdbedit によるユーザ追加も実施されていません。もちろん “smbuser" ユーザが未作成の場合はユーザの作成が行われ、changed のカウントが一つ増えます。

このように、一手間加えれば shell や command モジュールでも冪等性を保った Playbook を書くことが可能です。いかがでしょうか? 今回は samba を例に紹介しましたが iptables の設定であったりその他いろいろ応用が効くやり方かと思います。

さて、本題は以上なのですが、実際にこのようなタスクを書くにあたり、register で指定した変数にどのような値が格納されているのかが気になりますね。その場合は是非 debug モジュールを使ってみてください。Samba User List タスクの下に以下のような debug モジュールタスクを追加すると、変数の内容が表示されます。

- debug: var=samba_user_list

ansible-playbook を実行すると以下のように表示されます。

【"smbuser" ユーザ作成前】

TASK: [servers | debug var=samba_user_list] *******************************
ok: [samba-server] => {
    "item": "",
    "samba_user_list": {
        "changed": false,
        "cmd": "pdbedit -L | awk -F\":\" '{print $1}' ",
        "delta": "0:00:00.024101",
        "end": "2014-10-12 13:20:44.921479",
        "invocation": {
            "module_args": "pdbedit -L | awk -F\":\" '{print $1}'",
            "module_name": "shell"
        },
        "item": "",
        "rc": 0,
        "start": "2014-10-12 13:20:44.897378",
        "stderr": "",
        "stdout": "",
        "stdout_lines": []
    }
}

【"smbuser" ユーザ作成後】

TASK: [servers | debug var=samba_user_list] *******************************
ok: [samba-server] => {
    "item": "",
    "samba_user_list": {
        "changed": false,
        "cmd": "pdbedit -L | awk -F\":\" '{print $1}' ",
        "delta": "0:00:00.024101",
        "end": "2014-10-12 13:20:44.921479",
        "invocation": {
            "module_args": "pdbedit -L | awk -F\":\" '{print $1}'",
            "module_name": "shell"
        },
        "item": "",
        "rc": 0,
        "start": "2014-10-12 13:20:44.897378",
        "stderr": "",
        "stdout": "smbuser",
        "stdout_lines": [
            "smbuser"
        ]
    }
}

黄色で記載した部分が差分になります (見やすさのためにここでそのように色分けしただけで、ansible-playbook 実行時にこのような色で表示されるわけではありません)。このように実際に取得する情報の前後の差分を確認することで、実際に何を判定に用いればよいかが分かりますね。

なお、Ansible ドキュメントの Conditionals (条件分岐) では、register した文字型の変数に対して find 関数を使っていますが、find は対象の文字列に対して特定の文字列が含まれているかどうかを判定するものであるため、今回の場合だと仮に “smbuser2" というユーザがいると “smbuser" というユーザは作らないことになります。

配列に対して使用する count 関数は配列内から完全に一致するもののみの数を返してくれるため、今回はこちらを採用しました。

これらの関数は Ansible というよりは Python の関数なので、条件判定などをより細かく行いたい場合は Python の関数をいろいろ調べてみると良いのかもしれません。

一番便利なのは正規表現でマッチさせる方法なのですがその方法は調べてみましたがわかりませんでした。Ansible 内の register で保存した変数に対して正規表現でマッチをかける方法をご存じの方は教えて頂けると幸いです。

記事が書き終わりましたので冒頭に書きました Raspberry Pi B+ をどうしようかとさっそく考えてます。
家電をスマホで操作できるようにしようか、消費電力の少ない音楽配信サーバにしようか、
うちのお犬様がケージから脱出する姿を世の中に披露しようか。。。
悩んでるだけで、何もしなくなりそうなので、次の記事にRaspberry Piを掲載できるようにいじり倒します。

kamoAnsible,command,shell,冪等性

Posted by kamo