Ansibleの成長とディレクトリ構成について理解が追い付いてない

Posted on 2015/07/29(Wed) 23:45 in technical

はじめに

駄文です

サーバ構成管理ツールであるところのAnsibleのplaybookを、どのように適当に書いて整理するのが良いのか悩んでいる。

いくつかのサンプルケースを通じて、結局Ansibleのベストプラクティスに落ち着くかどうか。

とりあえずで書き始めたplaybookはもっと簡単に使いまわしたいんだ。

順を追おうと脳みそ垂れ流してたら、冗長で読みにくい感じになってしまった。

Phase.0

Ubuntu Server 14.04 amd64にAnsibleを入れて話を始める。

$ sudo apt update
$ sudo apt install -y python-pip python-dev sshpass
$ sudo pip install -U pip
$ sudo -H pip install ansible
$ ansible --version
ansible 1.9.2
  configured module search path = None
$ echo "[defaults]
host_key_checking = False" >> ~/.ansible.cfg

警告

ansibleインストール時に sudo -H しないで sudo だけで実行するとこうなる。:

The directory '/home/ansible/.cache/pip/http' or its parent directory is not owned by the current user and the cache has been disabled. Please check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag.

もちろん、 sudo しなかった場合は pycrypto をコンパイルできなかったりして死ぬ。

通報

host_key_checking = False を設定しない場合は、ログインする前のホスト鍵チェックで弾かれて以下のようなエラーが出るため、説明を容易にするため設定しておく。:

fatal: [127.0.0.1] => Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support this.  Please add this host's fingerprint to your known_hosts file to manage this host.

Phase.1

最初はごく単純なケースから始めよう。

あるサイト内に配置されている、自分自身をターゲットにNTPを設定する場合を考える。

適当に当該サイト用のディレクトリを作成し、playbookを作っていく。

$ mkdir example-playbook
$ cd example-playbook
$ echo "127.0.0.1" > hosts
$ grep -v -e "^#" -e "^$" ntp.conf
driftfile /var/lib/ntp/ntp.drift
statistics loopstats peerstats clockstats
filegen loopstats file loopstats type day enable
filegen peerstats file peerstats type day enable
filegen clockstats file clockstats type day enable
server 0.ubuntu.pool.ntp.org
server 1.ubuntu.pool.ntp.org
server 2.ubuntu.pool.ntp.org
server 3.ubuntu.pool.ntp.org
restrict -4 default kod notrap nomodify nopeer noquery
restrict -6 default kod notrap nomodify nopeer noquery
restrict 127.0.0.1
restrict ::1
disable monitor

$ cat ntp.yml
- hosts: all
  sudo: yes
  tasks:
  - name: ntp install
    apt: name=ntp update_cache=yes state=installed

  - name: ntp start
    service: name=ntp state=started enabled=yes

  - name: remove /var/lib/ntp/ntp.conf.dhcp
    file: path=/var/lib/ntp/ntp.conf.dhcp state=absent
    notify: ntpd-restart

  - name: upload ntp.conf
    copy: src=ntp.conf dest=/etc/ntp.conf
    notify: ntpd-restart

  handlers:
  - name: ntpd-restart
    service: name=ntp state=restarted

$ ls
hosts  ntp.conf  ntp.yml
$ ansible-playbook ntp.yml -i hosts -k -K
SSH password:
SUDO password[defaults to SSH password]:

PLAY [all] ********************************************************************

GATHERING FACTS ***************************************************************
ok: [127.0.0.1]

TASK: [ntp install] ***********************************************************
ok: [127.0.0.1]

TASK: [ntp start] *************************************************************
ok: [127.0.0.1]

TASK: [remove /var/lib/ntp/ntp.conf.dhcp] *************************************
changed: [127.0.0.1]

TASK: [upload ntp.conf] *******************************************************
changed: [127.0.0.1]

NOTIFIED: [ntpd-restart] ******************************************************
changed: [127.0.0.1]

PLAY RECAP ********************************************************************
127.0.0.1                  : ok=6    changed=3    unreachable=0    failed=0

警告

-k(--ask-pass)はsshログインパスワード、-K(--ask-sudo-pass)はsudo時のパスワードを対話的に入力できる。

上記の場合、-K(--ask-sudo-pass)は不要そうに思えるが、-Kが無ければMissing become passwordとエラーになることが想定される。

内容は、aptを用いてntpをインストールし、DHCPによって生成されたntp.confがあれば削除し、その後作成済みのコンフィグファイルをアップロードする。

DHCPによって生成されたntp.confを削除したか、アップロードしたファイルが元のファイルから変更することになった場合は、ntpサービスを再起動する。

ntp.confの中身は、ディストリビューションのデフォルト設定に disable monitor を追加した程度で、特にこれといって特別なものではない。

さて、これで新しくUbuntu server 14.04のホストが増えた場合でも、hostsに新しくIPを追加するだけで同じ設定を行うことが出来るようになった。

例えばこのようにすることで、同じ構成管理手順が2台のマシンに実行されるようになる。:

$ echo "192.168.122.101" >> hosts
$ ansible-playbook ntp.yml -i hosts -k -K

さて、次へいこう。

Phase.2

調子を良くしたところで、次はSNMPを加えてみよう。これまた設定ファイル自体はディストリビューションのデフォルトと大差無い。

$ grep -v -e "^#" -e "^$" -e "[ ]\+#" snmpd.conf
agentAddress udp:161,udp6:[::]:161
view   systemonly  included   .1.3.6.1.2.1.1
view   systemonly  included   .1.3.6.1.2.1.25.1
rocommunity public
sysLocation    Sitting on the Dock of the Bay
sysContact     Me <me@example.org>
sysServices    72
disk       /     10000
disk       /var  5%
disk       /boot 20%
includeAllDisks  10%
load   12 10 5
trapsink     localhost public
trap2sink    localhost public
informsink   localhost public
iquerySecName   internalUser
rouser          internalUser
linkUpDownNotifications  yes
master          agentx
$ cat snmpd.yml
- hosts: all
  sudo: yes
  tasks:
  - name: snmpd install
    apt: name=snmpd update_cache=yes state=installed

  - name: upload snmpd.conf
    copy: src=snmpd.conf dest=/etc/snmp/snmpd.conf
    notify:
      - snmpd-restart

  - name: snmpd start
    service: name=snmpd state=started enabled=yes

  handlers:
  - name: snmpd-restart
    service: name=snmpd state=restarted

さて、今のままではNTP、SNMPそれぞれの構成管理を実行するには ansible-playbook コマンドを2回叩かなければならない。例えばこう。:

$ ansible-playbook ntp.yml -i hosts -k -K
$ ansible-playbook snmpd.yml -i hosts -k -K

出来れば1回のコマンドで済ませたいが、 ntp.yml と snmpd.yml の内容を単に1つのファイルに書く、と言うのは先が思いやられる。

今回は、別途 site.yml を作成し、そこから各ファイルを include する形で対応してみよう。:

$ cat site.yml
- include: ntp.yml
- include: snmpd.yml
$ ansible-playbook site.yml -i hosts -k -K

これで、次に例えば smartmontools を入れたくなった場合は、新しく smartmontools.yml を作って、 include: smartmontools.yml と書けば良いことになる。

この場合、何も考えずに:

$ ansible-playbook ntp.yml -i hosts -k -K

のように実行することも出来る。

単体で動作するplaybookを試験的に作成し、全体のplaybookツリーに加えるという一連の流れがスムーズになるのは、個人的には非常に好ましい。

小休止

この時点で少しファイルが多くなってきたので、簡単な整理をする。

$ tree
.
├── hosts
├── ntp.conf
├── ntp.yml
├── site.yml
├── snmpd.conf
└── snmpd.yml

0 directories, 6 files
$ mkdir ntp
$ mv ntp.* ntp
$ mkdir snmpd
$ mv snmpd.* snmpd
$ cat site.yml
- include: ntp/ntp.yml
- include: snmpd/snmpd.yml
$ tree
.
├── hosts
├── ntp
│   ├── ntp.conf
│   └── ntp.yml
├── site.yml
└── snmpd
    ├── snmpd.conf
    └── snmpd.yml

2 directories, 6 files

まぁそこそこ綺麗だ。

Phase.3

更に、新しくCentOS7でサーバを構築することを計画してみよう。条件は次のようなことを考える。

  • IPアドレスは 192.168.122.132
  • 導入するサービスはNTPのみ(SNMPは入れない)

基本的には接続先がUbuntuならNTPとSNMP、CentOSならNTPだけを構成管理するように出来ればいい。

実現する方法はいくつか考えられるが、ここでは以下の方法について考えてみる。(もちろん、これで実現方法が全部なわけではない)

  1. hostsで分類しちゃう
  2. OSを検出して別々の処理をする(whenを使う方法)
  3. OSを検出して別々の処理をする(group_byを使う方法)

共通

何はともあれ、ホストの登録、CentOS用のntp.confの作成とファイル名の調整だけしておく。

$ echo "192.168.122.132" >> hosts
$ mv ntp/ntp.conf ntp/ntp.conf.ubuntu
$ grep -v -e "^#" -e "^$" -e "[ ]\+#" ntp/ntp.conf.centos
driftfile /var/lib/ntp/drift
restrict default nomodify notrap nopeer noquery
restrict 127.0.0.1
restrict ::1
server ntp1.jst.mfeed.ad.jp
server ntp2.jst.mfeed.ad.jp
server ntp3.jst.mfeed.ad.jp
includefile /etc/ntp/crypto/pw
keys /etc/ntp/keys
disable monitor

hostsで分類しちゃう

どのホストにどのOSまたはディストリビューションが入っていて、どう管理するかをある程度把握かつ管理できている場合に採用できなくもない方法。

hostsは重複したアドレスを書いても平気なので、こんな感じで。

$ cat hosts
[ubuntu_ntp]
127.0.0.1

[centos_ntp]
192.168.122.132

[ubuntu_snmp]
127.0.0.1

$ cat ntp/ntp.yml
- hosts: ubuntu_ntp
  sudo: yes
  tasks:
  - name: ntp install
    apt: name=ntp update_cache=yes state=installed

  - name: ntp start
    service: name=ntp state=started enabled=yes

  - name: remove /var/lib/ntp/ntp.conf.dhcp
    file: path=/var/lib/ntp/ntp.conf.dhcp state=absent
    notify: ntpd-restart

  - name: upload ntp.conf
    copy: src=ntp.conf.ubuntu dest=/etc/ntp.conf
    notify: ntpd-restart

  handlers:
  - name: ntpd-restart
    service: name=ntp state=restarted

- hosts: centos_ntp
  sudo: yes
  tasks:
  - name: ntp install
    yum: name=ntp update_cache=yes state=installed

  - name: ntp start
    service: name=ntpd state=started enabled=yes

  - name: upload ntp.conf
    copy: src=ntp.conf.centos dest=/etc/ntp.conf
    notify: ntpd-restart

  handlers:
  - name: ntpd-restart
    service: name=ntpd state=restarted

$ cat snmpd/snmpd.yml
- hosts: ubuntu_snmp
  sudo: yes
  tasks:
  - name: snmpd install
    apt: name=snmpd update_cache=yes state=installed

  - name: upload snmpd.conf
    copy: src=snmpd.conf dest=/etc/snmp/snmpd.conf
    notify:
      - snmpd-restart

  - name: snmpd start
    service: name=snmpd state=started enabled=yes

  handlers:
  - name: snmpd-restart
    service: name=snmpd state=restarted

で、各playbookのhostsを上記に合わせる。もちろん、playbookはincludeしてあればいいのでファイルが分かれても良い。

見るからに採用したくないが、非常に単純で見たまま対応できるので、小規模かつ深く考えたくない場合はこういった対応も可能だろう。

変数でこねくり回すより、理解は簡単だと思う。

OSを検出して別々の処理をする(whenを使う方法)

デフォルトで有効になっているgather_factsは、ログイン後にホストの情報を収集する。単発で動かすとこんな感じ。

$ ansible all -m setup -a "filter=ansible_distribution*" -i hosts -k
SSH password:
192.168.122.132 | success >> {
    "ansible_facts": {
        "ansible_distribution": "CentOS",
        "ansible_distribution_major_version": "7",
        "ansible_distribution_release": "Core",
        "ansible_distribution_version": "7.1.1503"
    },
    "changed": false
}

127.0.0.1 | success >> {
    "ansible_facts": {
        "ansible_distribution": "Ubuntu",
        "ansible_distribution_major_version": "14",
        "ansible_distribution_release": "trusty",
        "ansible_distribution_version": "14.04"
    },
    "changed": false
}

これは条件分岐にも使えるので、ansible_distributionを条件に処理を切り替えることが出来る。

今回はCentOSかUbuntuかを見分けた上で、異なる処理を書いたymlをincludeするように書いてみることを考える。

例えばこう(SNMPは一旦お休み)

$ cat site.yml
- include: ntp/ntp.yml

$ cat ntp/ntp.yml
- hosts: all
  sudo: yes
  tasks:
  - include: ntp.ubuntu.yml
    when: ansible_distribution == "Ubuntu"

  - include: ntp.centos.yml
    when: ansible_distribution == "CentOS"

  handlers:
  - include: ntp.ubuntu.handler.yml
    when: ansible_distribution == "Ubuntu"

  - include: ntp.centos.handler.yml
    when: ansible_distribution == "CentOS"

$ cat ntp/ntp.ubuntu.yml
- name: ntp install
  apt: name=ntp update_cache=yes state=installed

- name: ntp start
  service: name=ntp state=started enabled=yes

- name: remove /var/lib/ntp/ntp.conf.dhcp
  file: path=/var/lib/ntp/ntp.conf.dhcp state=absent
  notify: ntpd-restart

- name: upload ntp.conf
  copy: src=ntp.conf.ubuntu dest=/etc/ntp.conf
  notify: ntpd-restart

$ cat ntp/ntp.ubuntu.handler.yml
- name: ntpd-restart
  service: name=ntp state=restarted

$ cat ntp/ntp.centos.yml
- name: ntp install
  yum: name=ntp update_cache=yes state=installed

- name: ntp start
  service: name=ntpd state=started enabled=yes

- name: upload ntp.conf
  copy: src=ntp.conf.centos dest=/etc/ntp.conf
  notify: ntpd-restart

$ cat ntp/ntp.centos.handler.yml
- name: ntpd-restart
  service: name=ntpd state=restarted

めんどくさい!!!今まで書いてきたplaybookをバラすのがクソメンドクサイ!!!

そして実行

$ ansible-playbook site.yml -i hosts -k -K
SSH password:
SUDO password[defaults to SSH password]:

PLAY [all] ********************************************************************

GATHERING FACTS ***************************************************************
ok: [192.168.122.132]
ok: [127.0.0.1]

TASK: [ntp install] ***********************************************************
skipping: [192.168.122.132]
ok: [127.0.0.1]

TASK: [ntp start] *************************************************************
skipping: [192.168.122.132]
ok: [127.0.0.1]

TASK: [remove /var/lib/ntp/ntp.conf.dhcp] *************************************
skipping: [192.168.122.132]
changed: [127.0.0.1]

TASK: [upload ntp.conf] *******************************************************
skipping: [192.168.122.132]
ok: [127.0.0.1]

TASK: [ntp install] ***********************************************************
skipping: [127.0.0.1]
ok: [192.168.122.132]

TASK: [ntp start] *************************************************************
skipping: [127.0.0.1]
ok: [192.168.122.132]

TASK: [upload ntp.conf] *******************************************************
skipping: [127.0.0.1]
changed: [192.168.122.132]

NOTIFIED: [ntpd-restart] ******************************************************
skipping: [192.168.122.132]
changed: [127.0.0.1]

PLAY RECAP ********************************************************************
127.0.0.1                  : ok=6    changed=2    unreachable=0    failed=0
192.168.122.132            : ok=4    changed=1    unreachable=0    failed=0

skipが鬱陶しい。

このwhenを使ってincludeするymlを分ける方法は、Ansibleがrolesを用いたディレクトリ構成においても推奨しているっぽい方法なので、

今後のことを考えれば最初からこう書け、と言うことなのかもしれない。

参考: http://docs.ansible.com/ansible/playbooks_conditionals.html#applying-when-to-roles-and-includes

とは言え、この方式を採用した途端に全部のplaybookをバラして調整して、なんてことをしなければならないのだとしたら、力技で書いた方がまだマシに思える。

大体なんだこのskipの数は!抑制できないのか!いやまぁ、それは出来るんだけど。

参考: http://docs.ansible.com/ansible/intro_configuration.html#display-skipped-hosts

OSを検出して別々の処理をするようにグルーピングする(group_byを使う方法)

先程のwhenを使って切り替える方法は、いくつかの面倒が待っていた。

Ansibleのドキュメント にもこう書かれている:

You will note a lot of ‘skipped’ output by default in Ansible when using this approach on systems that don’t match the criteria. Read up on the ‘group_by’ module in the About Modules docs for a more streamlined way to accomplish the same thing.

と言うわけで、今度は group_by を使って書いてみる。(ntp/ntp.yml以外はwhenを使った場合と一緒)

$ cat ntp/ntp.yml
- hosts: all
  tasks:
  - group_by: key={{ ansible_distribution | lower }}

- hosts: ubuntu
  sudo: yes
  tasks:
  - include: ntp.ubuntu.yml

  handlers:
  - include: ntp.ubuntu.handler.yml

- hosts: centos
  sudo: yes
  tasks:
  - include: ntp.centos.yml

  handlers:
  - include: ntp.centos.handler.yml

$ ansible-playbook site.yml -i hosts -k -K
SSH password:
SUDO password[defaults to SSH password]:

PLAY [all] ********************************************************************

GATHERING FACTS ***************************************************************
ok: [192.168.122.132]
ok: [127.0.0.1]

TASK: [group_by key={{ ansible_distribution | lower }}] ***********************
changed: [127.0.0.1]

PLAY [ubuntu] *****************************************************************

GATHERING FACTS ***************************************************************
ok: [127.0.0.1]

TASK: [ntp install] ***********************************************************
ok: [127.0.0.1]

TASK: [ntp start] *************************************************************
ok: [127.0.0.1]

TASK: [remove /var/lib/ntp/ntp.conf.dhcp] *************************************
changed: [127.0.0.1]

TASK: [upload ntp.conf] *******************************************************
ok: [127.0.0.1]

NOTIFIED: [ntpd-restart] ******************************************************
changed: [127.0.0.1]

PLAY [centos] *****************************************************************

GATHERING FACTS ***************************************************************
ok: [192.168.122.132]

TASK: [ntp install] ***********************************************************
ok: [192.168.122.132]

TASK: [ntp start] *************************************************************
ok: [192.168.122.132]

TASK: [upload ntp.conf] *******************************************************
changed: [192.168.122.132]

NOTIFIED: [ntpd-restart] ******************************************************
changed: [192.168.122.132]

PLAY RECAP ********************************************************************
127.0.0.1                  : ok=8    changed=3    unreachable=0    failed=0
192.168.122.132            : ok=7    changed=3    unreachable=0    failed=0

skip無いけど縦に長い。

これはこれで使い勝手が良さそうだ。と言うか、この方式を採用するならrolesモデルと決別できるような気がする。

Phase.4

Ansibleのベストプラクティスと呼ばれるのは、rolesを用いたディレクトリ構造だ。

それは http://docs.ansible.com/ansible/playbooks_best_practices.html#directory-layout に描かれている。

このようなディレクトリ構造を最初から作って運用するのは、少々無理を感じる。

Phase.2までで書いていた単体のファイルで完結する記述から、Phase.3で使ったwhenやgroup_byを用いた記述に移行するのは正直メンドクサイ。

せめて今まで書いていたplaybookにタグを打ってファイル名を変える程度にしたい。

また、rolesを用いた構造にした場合、見分けに使用していたhosts:項が処理内容から遠くなってしまうのも頂けない。

それなら、rolesを使わないけどgroup_byは使う、こんな感じの方が分かりが良いんじゃないだろうか。

$ cat ntp/ntp.yml
- hosts: all
  tasks:
  - group_by: key={{ ansible_distribution | lower }}

- include: ntp.ubuntu.yml
- include: ntp.centos.yml

$ cat ntp/ntp.ubuntu.yml
- hosts: ubuntu
  sudo: yes
  tasks:
  - name: ntp install
    apt: name=ntp update_cache=yes state=installed

  - name: ntp start
    service: name=ntp state=started enabled=yes

  - name: remove /var/lib/ntp/ntp.conf.dhcp
    file: path=/var/lib/ntp/ntp.conf.dhcp state=absent
    notify: ntpd-restart

  - name: upload ntp.conf
    copy: src=ntp.conf.ubuntu dest=/etc/ntp.conf
    notify: ntpd-restart

  handlers:
  - name: ntpd-restart
    service: name=ntp state=restarted

$ cat ntp/ntp.centos.yml
- hosts: centos
  sudo: yes
  tasks:
  - name: ntp install
    yum: name=ntp update_cache=yes state=installed

  - name: ntp start
    service: name=ntpd state=started enabled=yes

  - name: upload ntp.conf
    copy: src=ntp.conf.centos dest=/etc/ntp.conf
    notify: ntpd-restart

  handlers:
  - name: ntpd-restart
    service: name=ntpd state=restarted

まぁ、これにしても結局はいくつかの分離と調整が入るのだが。

あと、whenを使わないでrolesモデルにOS識別を入れる方法に、こんなのがある。

$ cat site.yml
- hosts: all
  tasks:
  - group_by: key={{ ansible_distribution | lower }}

- hosts: ubuntu
  sudo: yes
  vars:
    os_type: ubuntu
  roles:
    - ntp
    - snmpd

- hosts: centos
  sudo: yes
  vars:
    os_type: centos
  roles:
    - ntp

$ cat roles/ntp/tasks/main.yml
- include: "{{ os_type }}.yml"

$ cat roles/snmpd/tasks/main.yml
- include: "{{ os_type }}.yml"

のだが、これまた冗長で面倒な感じだ。うーん、どうにもすわりが悪い。

とは言え、例えばWebとDBとAppと言うサーバ区分があって、その中でマルチディストリ環境だったとすると、単にディストリ名で分ければ良い訳でもないので、

-l(--limit)を使って対象ホストを制限するってことになって、roleが沢山あったらやっぱり複数コマンド叩くか、色々な条件分岐が必要になる。

少なくとも、先のケースのように hosts: 項目を占有していいかと言うとNGだろう。

かといって、全部のサーバをコマンド1個で、毎回全部再構成するんかいな、って感じでもあるので、どちらかと言えばバランスの問題な気がする。

うーん、もう少しユースケースを現実的にした方が良かったな。

悩みは続く。

Ansibleの動作自体は気に入っているのだけど、如何せん拡大に伴う思想に自分が追い付いていないのか、ベストプラクティスが見ていてつらい。

あっさり理解できるまで使ってみるしかないんだろうか。

それって長いなぁ。

こんなことで悩むはずでは無かったというのに。