Ansible 全体俯瞰

Ansible は、ドキュメントにあるサンプルとはちょっと違ったことをしようとすると、すぐにつまづいてしまいます。この Ansible の壁は、全体像が見えないためではないかと思っています。使っていくうちに、徐々に全体像が見えてくるものの、もっと早く知っていれば、ということが少なくありません。全体像を俯瞰するための構造など自分のメモとして残しておきますが、少しでも他の方の参考になれば幸いです。

1. Ansible の設定記法
2. コンフィグ(設定)ファイル ansible.cfg
3. Inventory (インベントリ)
4. Playbook (プレイブック)
5. Ansible のディレクトリ構造利用
6. import と include の違い
7. おまけ: Raspberry Pi3 (Ubuntu stretch 9) をコントロールホストにする

最低限、1. Ansible の設定記法、2. コンフィグ(設定)ファイル ansible.cfg、3. Inventory (インベントリ)、4. Playbook (プレイブック) を理解しておけば、ansible を使うことができると思います。(SSH の設定など、前提条件があります。)

使っていくうちに、複数の操作(tasks)をまとめて管理したり、再利用の場面が増えてきます。5. では、ディレクトリ構造を利用した ansible の利用方法を記載します。 


最後に、おまけで Raspberry Pi3 での利用方法を記載します。


# Ansible の設定記法
Ansible の設定では、[YAML 形式](https://ja.wikipedia.org/wiki/YAML) と [INI 形式](https://ja.wikipedia.org/wiki/INI%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB) (コメントに `#` を許す拡張 INI 記法) が使用できます。
両方の記載ができることで柔軟な書き方ができる一方、画一ではないために初めて利用すると混乱してしまいます。

以下のようなルールで覚えておくとよいでしょう。
1. `*.cfg` 、 `拡張子なしファイル` は、INI 形式
2. `*.yml` 、 `*.yaml` は YAML 形式

### INI 形式の例
`[ ]` で囲まれた「セクション名」と `=` で記載された「パラメータ」で構成されます。
`=` の前後に空白、TAB などを置くことができます。


```ini:./ansible.cfg
[defaults]
host_key_checking       = True

# シャープでコメントが書けます
[privilege_escalation]
become_ask_pass         = True

; セミコロンでコメントが書けます
[ssh_connection]
scp_if_ssh              = False
```

`=` の後ろは、True/False, yes/no という Boolean 以外は、行末まで「文字列」として扱われます。
`"` などで囲むと、 `"` 自体が文字列の一部として解釈される場合があります。

「リスト」(配列)は、カンマ(`,`)で区切ります。

### YAML 形式の例
`:` で区切られた「名前」の列挙を「スカラ」と呼びます。

```YAML:スカラ
name1:
name2:
:
nameN:
```

というように、頭に `-` をつけずに列挙すると「直列」(単純列挙, 「スカラ」)になります。

`:` の後ろに値を書くことができます。辞書型とも呼ばれますが、YAML ではこれを「マッピング」と呼びます。

```YAML:マッピング
name1: aaaa
name2: bbbb
:
nameN: cccc
```

頭に `- ` をつけた場合には、リスト(配列)になります。YAML では、これを「シーケンス」と呼びます。

```YAML:シーケンス
- name1:
- name2:

- nameN:
```

インデントによって、階層を表現し、これを「コレクション」と呼びます。
インデントには空白を使います。TAB をそのままで使うとエラーになりますので、注意してください。
(vi/vim を使用している場合は、`set expandtab` で設定しておくと、TAB が空白に置き換わります)
 

```yaml:例-playbook-samle1.yml
- name: the 1st Example
  hosts: 172.17.0.2
  tasks:
    - name: Hello, World!
      debug:
    - name: Next debug
      debug: msg="Hello, Ansible!"
```

この例では `name: the 1st Example` 、 `hosts: 172.17.0.2` 、 `tasks:` は同じレベルのスカラ(並列(順次))設定、 `- name: Hello, ansible` 、 `- name: Next debug` の部分がシーケンス(リスト(配列))です。それぞれが値を持つ、マッピングになっています。
`tasks` は、コレクションです。

YAML形式で、よく冒頭に `---` (ハイフン3つ) が書かれているものを見かけます。
これは、YAML の構造が、一つの定義ブロックを `---` で始まり、`...` (ドット3つ) で終わることになっているためです。
`---` によって、一つのファイルに複数のブロックを記述することができるのですが、一般的に、ansible では、そのような構造を使用しません。
`---` を付けておけば無難と考えておけばよいでしょう。上の `playbook-samle1.yml` を書き直すと下のようになります。

```yaml:playbook-samle1.yml
---
- name: the 1st Example
  hosts: 172.17.0.2
  tasks:
    - name: Hello, World!
      debug:
    - name: Next debug
      debug: msg="Hello, Ansible!"
```

# コンフィグ(設定)ファイル ansible.cfg
参照: [Ansible Configuration Settings — Ansible Documentation](https://docs.ansible.com/ansible/latest/reference_appendices/config.html)
コンフィグファイルは、拡張子が `.cfg` ですので、 INI 形式で記載します。
ansible 関連のコマンドは以下の順序でコンフィグファイルを検索し、最初に見つかったコンフィグファイルが使用されます。

1. 環境変数 `ANSIBLE_CONFIG` で指定したファイル
2. カレントディレクトリの `ansible.cfg` (`./ansible.cfg`)
3. ホームディレクトリの `.ansible.cfg` (`~/.ansible.cfg` または `$HOME/.ansible.cfg)
4. /etc/ansible/ansible.cfg

### ansible.cfg の例

```ini:ansible.cfg
[defaults]
inventory		= ./inventory
interpreter_python	= /usr/bin/python3
#deprecation_warnings	= False

[privilege_escalation]
become_ask_pass		= yes

[ssh_connection]
scp_if_ssh		= False
```

# Inventory (インベントリ)
参照: [How to build your inventory — Ansible Documentation](https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html)
ansible コマンドの実行や、後述の Playbook の操作を行うためには、Inventory (インベントリ)と呼ばれる操作対象ホスト等を記載したファイルを用意する必要があります。簡単に一言で言ってしまえば、/etc/hosts ファイルの集合のようなものです。

* 各 Ansible コマンドに共通で `-i` というオプションで Inventory ファイル、または Inventory ファイルが用意されたディレクトリを指定します。コンフィグファイルに設定することでオプションを省略できます。([`defaults`] セクションの `inventory` パラメータ)
* 静的な Inventory ファイルは、INI形式、YAML 形式いずれの形式でも記載できます。加えて、 `*.py` や `*.sh` などのスクリプトを指定して、操作実行時に動的に構成することが可能です(正しい形式で返す必要があります)。

### Inventory の例
* `myhosts1` ファイル

```text:myhost1
172.17.0.2
```
一つの IP アドレス (host) だけが書かれたものが最小の Inventory ファイルになります。

* `myhosts2` ファイル

```ini:myhost2
172.17.0.2
172.17.0.3

[group1]
172.17.0.2
172.17.0.3

[group2]
172.17.0.[3:7]

[my_all:children]
group1
group2
```

* 各 `[ ]` はグループを指定します。`children` は、**予約語** で、子のグループを指定する場合に使用します。
* その他 `[group名:vars]` で、グループに適用する変数を記載します。`vars` は **予約語** です。


# Playbook (プレイブック)
`ansible -m ping hostpattern` というアドホックな(＝単発の)モジュール指定と操作ではなく、Playbook と呼ばれる設定ファイルを指定して、一連の操作を実行します。
Playbook の形式は YAML 形式が一般的です。

### Playbook の例

```yaml:playbook-sample1.yml
- name: the 1st Example
  hosts: 172.17.0.2
  tasks:
# タスクは、リスト(シーケンス)で指定する。
# デバッグで、デフォルトのメッセージを表示させる。
    - name: Hello, World
      debug:
# デバッグで、指定したメッセージを表示させる。
    - name: Next debug
      debug: msg="Hello, Ansible!"
```

Playbook を実行するためには、インベントリファイルを指定する必要があります。

```ini:myhosts1
172.17.0.2
```

* 実行例

```shell-session
$ ansible-playbook -i myhosts1 playbook-sample1.yml

PLAY [the 1st Example] *************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************
ok: [172.17.0.2]

TASK [Hello, World] ****************************************************************************************************
ok: [172.17.0.2] => {
    "msg": "Hello world!"
}

TASK [Next debug] ******************************************************************************************************
ok: [172.17.0.2] => {
    "msg": "Hello, Ansible!"
}

PLAY RECAP *************************************************************************************************************
172.17.0.2                 : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

```



# Ansible のディレクトリ構造
参照: [Roles — Ansible Documentation](https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html)
以下のディレクトリ構造を利用することで、ファイルの指定、タスク(tasks)の指定などが楽になります。

```
./
├─ ansible.cfg  ……………………………… (1) コンフィグファイル。環境変数 ANSIBLE_CONFIG で
│                                明示的に指定するのでなければ名前固定。
├─ playbook.yml ……………………………… (2) Playbook 任意のファイル名。YAML 形式が一般的。
│
├─ inventory/ …………………………………… (3) Inventory ディレクトリ。必ず指定する必要があり、
│    │                           任意の名前。オプションで指定する。
│    ├─ hosts …………………………………… (4) 任意の名前。拡張子がなければ INI 形式、
│    ├─ hosts2.yml               拡張子が .yml であれば YAML 形式。
│    ├─ : 
│    └─ hostsN
├─ group_vars/ ………………………………… (5) 指定したタスクの「対象ホストグループ」に適用される変数を
│    │                           格納するディレクトリ。名前固定。
│    ├─ groupname1 ……………………… (6) 「対象ホストグループ」名をファイル名にする。拡張子がなければ INI 形式、
│    ├─ groupname2.yml           YAML 形式であれば、拡張子を .yml にする。
│    ├─ : 
│    └─ groupnameN
├─ host_vars/ …………………………………… (7) 指定したタスクの「対象ホスト」に適用される変数を
│    │                           格納するディレクトリ。名前固定。
│    ├─ hostname1 ………………………… (8) 「対象ホスト」名をファイル名にする。拡張子がなければ INI 形式、
│    ├─ hostname2.yml            YAML 形式であれば、拡張子を .yml にする。
│    ├─ : 
│    └─ hostnameN
├─ files/ ……………………………………………… (9) コピー、転送などに用いるファイル置き場。名前固定。
│    ├─ file1 ……………………………………(10) 任意のファイル名。
│    ├─ file1
│    ├─ :
│    └─ fileN
└─ roles/ ………………………………………………(11) プレイブックで roles を使用する場合の設定ディレクトリ。名前固定。
    ├─ role1/ ……………………………………(12) 「対象ロール」名をディレクトリ名にする。
    ├─ role2/
    ├─ :
    └─ roleN/
         ├─ defaults …………………(13) デフォルトの変数設定を格納するディレクトリ。名前固定。
         │   └── main.yml ……(14) 最初に読み込まれる変数設定ファイル。名前固定。
         │                       import_vars と同様に、Playbook のパース時に読み込まれる。
         ├─ vars ……………………………(15) 変数設定を格納するディレクトリ。名前固定。
         │    │                  相対パスでファイル指定時のベースディレクトリ。
         │    ├─ varfile1 ……(16) 任意のファイル名。拡張子がなければ INI 形式、
         │    ├─ varfile2.yml    YAML 形式であれば、拡張子を .yml にする。
         │    ├─ :
         │    └─ varfileN
         └─ tasks …………………………(17) タスクを格納するディレクトリ。名前固定。
             └── main.yml ……(18) 最初に実行されるタスク設定ファイル。名前固定。
                                 対象ホストの OS で処理を変えたい場合には、
                                 main.yml から import/include で読み込む。
```

他にも `handlers/` 、 `templates/` 、`meta/` などのディレクトリで役割がありますが、ひとまずは、上のディレクトリ構造を理解しておけば、他のディレクトリも理解しやすくなると思います。

さらに詳しい説明は、"[Roles — Ansible Documentation](https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html)" をご覧ください。
"[Best Practices: Directory Layout — Ansible Documentation](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html#directory-layout)" にも例があります。

# import と include の違い
参照: [Including and Importing — Ansible Documentation](https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_includes.html)
import* も、include* も、どちらもファイルを読み込むことには違いはありませんが、読み込むタイミングが異なります。

> * All import* statements are pre-processed at the time playbooks are parsed.
> * All include* statements are processed as they are encountered during the execution of the playbook.

* import_* (import, import_tasks, import_vars, import_role, import_playbook) は、Playbook のパース時に処理される。
* include_* (include, include_tasks, include_vars, include_role) は、タスクの実行時に処理される。

特に重要になるのは、JINJA2 のテンプレートを使った変数 (例えば {{ samplevarname }} というテンプレート) の処理や、変数への代入がいつ行われるか、ということです。

import* を使用すると、タスクが実行される前に展開されていますので、組み込まれたテンプレートは、実際の値に置き換わっていて、変数への代入が終わってからタスクが実行されます。

一方で、include* を使用すると、展開されるのはタスクが実行されているその時ですので、読み込まれて実行されているタイミングで、テンプレートが実際の値に置き換わり、代入が行われます。



# おまけ: Raspberry Pi3 (Ubuntu stretch 9) をコントロールホストにする

Ansible は Python 環境で利用しますが、Raspberry Pi3 の Ubuntu(stretch 9) 上で OS ベースの ansibule をインストール (apt/apt-get install) すると Version 2.2 の Ansible がインストールされます (執筆 2020年1月13日時点)。

v2.2 では最新の機能が使えず、少し不便ですので、Python の仮想環境でインストール(pip install)することで最新の Ansible (執筆時点で最新は v2.9) が利用できます。(Raspberry Pi3/Ubuntu の Python3 のバージョンは、3.5 です)

```shell-session
$ cat /etc/os-release
PRETTY_NAME="Raspbian GNU/Linux 9 (stretch)"
NAME="Raspbian GNU/Linux"
VERSION_ID="9"
VERSION="9 (stretch)"
VERSION_CODENAME=stretch
ID=raspbian
ID_LIKE=debian
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"
$ python3 --version
Python 3.5.3
```

Python 仮想環境でのインストールは以下の通り。

```shell-session
$ python3 -m venv ansible
$ source ansible/bin/activate
$ pip install -U pip
$ pip install ansible
```
`python3 -m venv` の後ろの *ansible* と `source` コマンドで指定している部分の *ansible* は同じもので、ご自分で自由に決めることができます。
`pip install` で指定している `ansible` は、インストールするパッケージですので、変更せずにこのまま実行します。


#### :point_up: alias の利用
bash を利用している場合には、`~/.bashrc` に次のような設定を行うと、便利です。

```bash:~/.bashrc&nbsp;または&nbsp;~/.bash_aliases&nbsp;など
alias ansibleenv='type deactivate > /dev/null 2>&1 && deactivate; cd ~/work/Ansible.d/; source ~/work/Python.d/envs/ansible/bin/activate'
```

`type deactivate > /dev/null 2>&1 && deactivate` の部分で、他の環境で activate されていた場合には、deactivate します。
`cd ~/work/Ansible.d` で、作業用のディレクトリに移動します。
`source ~/work/Python.d/envs/ansible/bin/activate` で、ansible の環境を activate しています。
設定ができていたら、`ansibleenv` で呼び出せます。

### :point_up: weakref.py のエラー修正
Raspberry Pi 上では、ansible 自体の実行が成功しても、以下のようなエラーが発生することがあります。

```
：
PLAY RECAP *************************************************************************************************************
172.17.0.2                 : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Exception ignored in: <function WeakValueDictionary.__init__.<locals>.remove at 0x75c7fdf8>
Traceback (most recent call last):
  File "/usr/lib/python3.5/weakref.py", line 117, in remove
TypeError: 'NoneType' object is not callable
Exception ignored in: <function WeakValueDictionary.__init__.<locals>.remove at 0x75c7fdf8>
Traceback (most recent call last):
  File "/usr/lib/python3.5/weakref.py", line 117, in remove
TypeError: 'NoneType' object is not callable
    :
    :
Exception ignored in: <function WeakValueDictionary.__init__.<locals>.remove at 0x75c7fdf8>
Traceback (most recent call last):
  File "/usr/lib/python3.5/weakref.py", line 117, in remove
TypeError: 'NoneType' object is not callable
```

これは、 ```/usr/lib/python3.5/weakref.py``` の変数が定義されていない(タイプ "None")ために起きるエラーです。
根本的な解決は呼び出し側で考慮する必要がありますが、暫定的に以下のパッチの適用で回避できます。(かなり乱暴ですが)

```diff:weakref.py.patch
*** weakref.py.bak      2018-09-28 02:25:39.000000000 +0900
--- weakref.py  2020-01-03 18:44:49.190027705 +0900
***************
*** 114,120 ****
                  else:
                      # Atomic removal is necessary since this function
                      # can be called asynchronously by the GC
!                     _remove_dead_weakref(d, wr.key)
          self._remove = remove
          # A list of keys to be removed
          self._pending_removals = []
--- 114,121 ----
                  else:
                      # Atomic removal is necessary since this function
                      # can be called asynchronously by the GC
!                     if type(_remove_dead_weakref) is not type(None) and type(d) is not type(None) and type(wr) is not type(None):
!                         _remove_dead_weakref(d, wr.key)
          self._remove = remove
          # A list of keys to be removed
          self._pending_removals = []
```


パッチの適用:

```shell-session
$ sudo patch -c -b /usr/lib/python3.5/weakref.py weakref.py.patch
```

`-c` はコンテキストタイプのパッチ適用オプション、 `-b` はバックアップ作成オプションです。