Ansibleでコマンド引数が異なる複数マシンの構成を行う & 一度に複数のコマンドを発行する

Ansibleを使うと、１つのマシンから複数のマシンの構成を行うことができます。

構成するためのコマンドの引数が各マシンで異なる場合は、それを変数化して処理します。この記事では、IBM の Db2 の導入・構成を例に、そのやり方を説明します。
また、Db2へ接続した後、別のタスクでSQL文を発行すると、Db2への接続が切れてSQL文がエラーとなるため、1つのタスクでそれらの処理を行う必要があります。この記事では、そのやり方についても説明します。

# ポイント
- マシンによらず固定値またはシェル・スクリプトで生成するような値は、Ansible Playbook(yamlファイル)に変数を定義することができます。
    - この記事の例では、
        - 導入時のログのファイル名にホスト名と日付を付与
        - DB接続に必要なDB名、ユーザーID、パスワードを変数化
- マシンごとに異なる値を定義したい場合は、ホスト名のリストとともに変数を定義することができます(この記事の例では、`inventories/hosts.ini`ファイルに定義)。
    - この記事の例では、
        - Db2サーバーとDb2クライアントで異なる導入定義ファイルを指定
        - Db2クライアントのマシンごとに、異なるDB接続先ホスト名、ポート番号を指定

# シナリオ
この記事の例では、Db2サーバーは2つのマシンで構成され、それぞれ2つのノードで動作し、各マシンの1つ目のノードは50000ポート、2つ目のノードは50001ポートでDb2クライアントからのリクエストを受け付けます。

![ansible_scenario.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507588/fd747b56-082b-c7b8-78de-9af4caee3cca.png)

Db2クライアントとして4つのマシンからDb2サーバーにアクセスします。各クライアントからのアクセスは負荷分散するために、以下のDb2サーバーにアクセスするようにします。

| Db2クライアント | Db2サーバー接続先 |
|:--------------|:----------------|
| host3         |host1の50000ポート|
| host4         |host1の50001ポート|
| host5         |host2の50000ポート|
| host6         |host2の50001ポート|

この記事では、上記を実現するための構成の一部である「1. Db2の導入」と「2. Db2クライアントの接続構成」、およびDb2接続確認を行う「3. Db2クライアントからDb2接続」を説明します。

その前に、Ansibleのセットアップと、Ansibleのシンプルな実行例を述べます。

# Ansibleのセットアップ
Ansibleの実行は、上記のうち1つのサーバーから実行するか、別のマシンから実行することができます。

実行するマシンにはAnsibleを導入し、構成するすべてのサーバーに対して、rootでパスワードレスでアクセスできるようにします。

**Ansibleの導入例 (Red Hat 9.2の場合)**
```
# dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm -y
# yum install ansible -y
```

# Ansibleのシンプルな実行例
シンプルな例として、Db2が導入されているか確認するために、すべてのマシンに対して`ls /opt/ibm/db2`を実行します。
まずは、`inventories/hosts.ini`ファイルにマシンのリストを記述します。
(この例では、以下の3つのマシンに対して実行します)
```ini:inventories/hosts.ini
[db2hosts]
host1
host2
host3
```
実行する内容はAnsible Playbookと呼ばれるyamlファイルに記載します。この例では、ファイル名を`db2_check.yml`とし、以下を記載します。
```yml:db2_check.yml
- name: db2 check
  hosts: all
  tasks:
    - name: ls /opt/ibm/db2
      shell:
        cmd: ls /opt/ibm/db2
      register: result
    - name: result
      debug:
        msg: "{{ result.stdout }}"
```

![ansible_db2_check_yml.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507588/4a9c4fcc-6abe-2a4e-93c1-bbee64e9824f.png)

上記のAnsible Playbookを実行するために、以下のコマンドを実行します。

```shell
# ansible-playbook -i inventories/hosts.ini db2_check.yml
```

結果として、以下のように出力されます。
```
PLAY [db2 check] ***********************************************************************************************************************************************

TASK [Gathering Facts] *****************************************************************************************************************************************
ok: [host2]
ok: [host3]
ok: [host1]

TASK [ls /opt/ibm/db2] *****************************************************************************************************************************************
fatal: [host3]: FAILED! => {"changed": true, "cmd": "ls /opt/ibm/db2", "delta": "0:00:00.011545", "end": "2024-09-08 19:23:04.972991", "msg": 
"non-zero return code", "rc": 2, "start": "2024-09-08 19:23:04.961446", "stderr": "ls: '/opt/ibm/db2' にアクセスできません: そのようなファイルやディレクトリは
ありません", "stderr_lines": ["ls: '/opt/ibm/db2' にアクセスできません: そのようなファイルやディレクトリはありません"], "stdout": "", "stdout_lines": []}
changed: [host1]
changed: [host2]

TASK [result] **************************************************************************************************************************************************
ok: [host1] => {
    "msg": "V11.5"
}
ok: [host2] => {
    "msg": "V11.5"
}

PLAY RECAP *****************************************************************************************************************************************************
host1     : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
host2     : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
host3     : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
```

上記出力から、host3には`/opt/ibm/db2`にDb2が導入されておらず、host1とhost2には少なくとも`/opt/ibm/db2/V11.5`ディレクトリまたはファイルが作成されていることがわかります。

:::note info
コマンドの出力を表示させるのに、上記では `.stdout` を使っています。この場合、改行の代わりに `¥n` が表示されます。改行させたい場合は、`.stdout_lines` と記述するか、`.stdout.splitlines()` と記述します。
:::

# シナリオの実行
以下の3つのAnsible Playbookを作成・実行します。
## 1. Db2の導入
### 設計
すべてのマシンにDb2を導入します。
Db2サーバーの導入にはDb2サーバー用にパラメータ設定された `db2server.rsp` ファイルを指定して導入し、Db2クライアントの導入にはDb2クライアント用にパラメータ設定された `db2client.rsp` ファイルを指定して導入します。
Db2導入コマンドdb2setupファイルは、事前に、すべてのマシンにNFSマウントされた `/download/db2` ディレクトリ下に置きます。
`db2server.rsp`と`db2client.rsp`ファイルは、Ansibleを実行するディレクトリに置き、Ansible Playbookで各マシンの`/db2install`ディレクトリにコピーします。
Db2の導入ログを `/log/db2_install_[ホスト名]_[YYYYMMDD].log` ファイルに取得し、Ansible Playbook実行マシンの `/log` ディレクトリ下にコピーします。
Db2の導入コマンドは以下のようになります。
- Db2サーバー
  - 実行ディレクトリ: /download/db2
  - 実行コマンド: ./db2setup -r /db2install/db2server.rsp -l "/log/db2\_install\_\`hostname\`\_\`date +%Y%m%d\`.log"
- Db2クライアント
  - 実行ディレクトリ: /download/db2
  - 実行コマンド: ./db2setup -r /db2install/db2client.rsp -l "/log/db2\_install\_\`hostname\`\_\`date +%Y%m%d\`.log"

### Ansibleの構築
実行コマンドで、マシンごとに異なるパラメータは以下となります。
1. Db2サーバーは `db2server.rsp`、Db2クライアントは `db2client.rsp` を指定する。
2. すべてのマシンで、"/log/db2\_install\_\`hostname\`\_\`date +%Y%m%d\`.log" を指定する。

1は導入するマシンで決まります。その場合、`inventories/hosts.ini` ファイルにその変数を記載することができます。

この例では、Db2サーバーとDb2クライアントで値が異なるので、以下のように、すべてのホストをその2つに分類します。
```
[db2server]
host1
host2
[db2client]
host3
host4
host5
host6
```
さらに、以下のように、上記に続けて変数を記載します。
```
[db2server:vars]
rsp_file=db2server.rsp
[db2client:vars]
rsp_file=db2client.rsp
```
この例では、`rsp_file`が変数名になります。

2は、マシンによらずコマンドの実行結果を変数の値としてセットするものです。その場合、Ansible Playbook(yamlファイル)に以下のように記載します。
```
  tasks:
    - name: log name
      shell:
        cmd: echo "/log/db2_install_`hostname`_`date +%Y%m%d`.log"
      register: log_name
```
上記の`log_name`が変数名で、`log_name.stdout`に標準出力の値がセットされます。

Db2の導入は、以下のように記載します。
```
    - name: db2 install
      shell:
        cmd: ./db2setup -r "/db2install/{{ rsp_file }}" -l "{{ log_name.stdout }}"
        chdir: /download/db2
```

Db2導入実行後のログファイルの取得は、以下のように記載します。
```
    - name: fetch log file
      fetch:
        src: "{{ log_name.stdout }}"
        dest: "/log/"
        flat: yes
```
`flat:`の値を`yes`にすると、`dest:`の値が/で終わっている場合、そのディレクトリ下に`src:`のファイルがコピーされます。

上記をまとめると、`inventories/hosts.ini`は、以下のようになります。
```ini:inventories/hosts.ini
[db2server]
host1
host2
[db2client]
host3
host4
host5
host6
[db2server:vars]
rsp_file=db2server.rsp
[db2client:vars]
rsp_file=db2client.rsp
```
Ansible Playbookは以下のようになります(この例では、ファイル名を`db2_install.yml`とします)。
```yaml:db2_install.yml
- name: db2 install
  hosts: all
  tasks:
    - name: log name
      shell:
        cmd: echo "/log/db2_install_`hostname`_`date +%Y%m%d`.log"
      register: log_name
    - name: copy rsp file
      copy:
        src: "{{ rsp_file }}"
        dest: /db2install/
        owner: root
        group: root
        mode: 0644
    - name: db2 install
      shell:
        cmd: ./db2setup -r "/db2install/{{ rsp_file }}" -l "{{ log_name.stdout }}"
        chdir: /download/db2
    - name: fetch log file
      fetch:
        src: "{{ log_name.stdout }}"
        dest: "/log/"
        flat: yes
```
このAnsible Playbookを実行するために、以下のコマンドを実行します。
```shell
# ansible-playbook -i inventories/hosts.ini db2_install.yml
```
## 2. Db2クライアントの接続構成
### 設計
Db2クライアントの接続構成は、Db2インスタンス・ユーザー(この記事では`db2inst1`ユーザーとします)で行います。
Ansibleでリモート接続した後、ユーザーを切り替えるには以下のように`become`と`become_user`を指定します。
```
- name: db2client config
  hosts: db2client
  become: yes
  become_user: db2inst1
```
しかしながら、この方法でユーザーを切り替えると、ユーザーのプロファイルが実行されません (`su - db2inst1`ではなく、`su db2inst1`で切り替えられる)。結果として、Db2のコマンドの実行でエラーとなります。

そこで、上記の代わりに、各コマンドの実行で、`su - db2inst1 -c コマンド` で対応することとします。

ここで使用するコマンドは各マシンでパラメータが異なり、以下のようになります。
| ホスト名 | コマンド |
|:--------|:-------|
| host3 | db2 catalog tcpip node db2svr remote host1 server 50000 |
| host4 | db2 catalog tcpip node db2svr remote host1 server 50001 |
| host5 | db2 catalog tcpip node db2svr remote host2 server 50000 |
| host6 | db2 catalog tcpip node db2svr remote host2 server 50001 |

上記でマシンごとの違いは、接続先のホスト名とポート番号になります。

| Db2クライアント | 接続先ホスト | 接続先ポート |
|:--------------|:-----------|:-----------|
| host3         | host1      | 50000      |
| host4         | host1      | 50001      |
| host5         | host2      | 50000      |
| host6         | host2      | 50001      |

### Ansibleの構築
「1. Db2の導入」では、`inventories/hosts.ini`でdb2serverとdb2clientに分類しましたが、ここでは、以下のようにdb2clientをさらに分類し、個々のマシンに対して上記の変数を定義します。
```ini:inventories/hosts.ini
[db2server]
host1
host2
[db2client:children]
client1
client2
client3
client4
[db2server:vars]
rsp_file=db2server.rsp
[db2client:vars]
rsp_file=db2client.rsp
[client1]
host3
[client2]
host4
[client3]
host5
[client4]
host6
[client1:vars]
db2host=host1
db2port=50000
[client2:vars]
db2host=host1
db2port=50001
[client3:vars]
db2host=host2
db2port=50000
[client4:vars]
db2host=host2
db2port=50001
```

![ansible_hosts_ini.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507588/62b6b342-733f-6b98-9d0f-f4de185a1d93.png)

Ansible Playbookは以下のようになります(この例では、ファイル名を`db2client_config.yml`とします)。
```yaml:db2client_config.yml
- name: db2client config
  hosts: db2client
  tasks:
    - name: catalog node
      shell:
        cmd: su - db2inst1 -c 'db2 catalog tcpip node db2svr remote "{{ db2host }}" server "{{ db2port }}"'
```
実行するマシンはDb2クライアントのみですので、hosts: には db2client をセットします。

このAnsible Playbookを実行するために、以下のコマンドを実行します。
```shell
# ansible-playbook -i inventories/hosts.ini db2client_config.yml
```

## 3. Db2クライアントからDb2接続
Db2クライアントからDb2サーバーに接続するためには、Db2のインスタンス・ユーザーから、以下のコマンドを実行します。
```
$ db2 connect to [DB名] user [ユーザー名] using [パスワード]
```
DB名、ユーザー名、パスワードを変数化する場合、Ansible Playbookは以下のようになり(ファイル名を`db2client_connect.yml`とします)、変数値は`vars:`に指定します。
```ini:db2client_connect.yml
- name: db2 connect
  hosts: db2client
  vars:
    dbname: "db1"
    user: "db2inst1"
    pwd: "password"
  tasks:
    - name: db2 connect
      shell:
        cmd: su - db2inst1 -c 'db2 connect to "{{ dbname }}" user "{{ user }}" using "{{ pwd }}"'
      register: result
    - name: result
      debug:
        msg: "{{ result.stdout_lines }}"
```
このAnsible Playbookを実行するために、以下のコマンドを実行します。
```shell
# ansible-playbook -i inventories/hosts.ini db2client_connect.yml
```

# Db2接続後に一連のSQL文を発行
冒頭で述べた通り、Db2へ接続した後、別のタスクでSQL文を発行すると、Db2への接続が切れてSQL文がエラーとなるため、1つのタスクでそれらの処理を行う必要があります。
そのためには、`su - db2inst1 -c コマンド` のコマンドに複数のコマンドを `&&` で区切って記述します。以下のように `cmd: >` または `cmd: |` とすると、複数行に渡って記述できるので便利です。
```ini:db2client_select.yml
- name: db2 connect
  hosts: db2client
  vars:
    dbname: "db1"
    user: "db2inst1"
    pwd: "password"
  tasks:
    - name: db2 connect
      shell:
        cmd: >
          su - db2inst1 -c '
            db2 connect to "{{ dbname }}" user "{{ user }}" using "{{ pwd }}" &&
            db2 "select * from t1" &&
            db2 terminate
          '
      register: result
    - name: result
      debug:
        msg: "{{ result.stdout_lines }}"
```

# インベントリーの親子関係と変数定義の確認

以下のように、`ansible-inventory`コマンドでインベントリーの親子関係と変数定義を確認することができます。
```
# ansible-inventory --graph --vars -i inventories/hosts.ini 
@all:
  |--@ungrouped:
  |--@db2server:
  |  |--teljpdb41.fyre.ibm.com
  |  |  |--{rsp_file = db2server.rsp}
  |  |--teljpdb42.fyre.ibm.com
  |  |  |--{rsp_file = db2server.rsp}
  |  |--{rsp_file = db2server.rsp}
  |--@db2client:
  |  |--@client1:
  |  |  |--teljpdb43.fyre.ibm.com
  |  |  |  |--{db2host = host1}
  |  |  |  |--{db2port = 50000}
  |  |  |  |--{rsp_file = db2client.rsp}
  |  |  |--{db2host = host1}
  |  |  |--{db2port = 50000}
  |  |--@client2:
  |  |  |--teljpdb31.fyre.ibm.com
  |  |  |  |--{db2host = host2}
  |  |  |  |--{db2port = 50001}
  |  |  |  |--{rsp_file = db2client.rsp}
  |  |  |--{db2host = host2}
  |  |  |--{db2port = 50001}
  |  |--{rsp_file = db2client.rsp}
```

# 参考文献
- [Ansibleのインストール](https://docs.ansible.com/ansible/2.9_ja/installation_guide/intro_installation.html)
- [変数の使用](https://docs.ansible.com/ansible/2.9_ja/user_guide/playbooks_variables.html)
- [インベントリーの構築方法](https://docs.ansible.com/ansible/2.9_ja/user_guide/intro_inventory.html)
- [ansible.builtin.copy module – Copy files to remote locations](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/copy_module.html)
- [ansible.builtin.fetch module – Fetch files from remote nodes](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/fetch_module.html)
- [ansible.builtin.debug module – Print statements during execution](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/debug_module.html)
- [ansible-inventory](https://docs.ansible.com/ansible/latest/cli/ansible-inventory.html)