nakayumcの技術ブログ

エンジニア2年目のブログです。

serialディレクティブと、run_once + delegate_to を付けたタスクを実行したらハマった

はじめに

Ansibleでは、特に何も指定しなければ、1度に全てのノードに対し処理を実行しますが、
全てのノードに対して処理を実行すると、コントロールノードの負荷が高くなり、処理速度が遅くなる可能性があります。

そのため、serial ディレクティブを使って、指定した台数づつ処理を実行させたところ、見事にハマったので備忘録として残しておきます。

環境

  • Ansible 2.16.0
  • Python 3.10.2
  • 適当に作成したLinuxサーバ4台

serial ディレクティブとは

Ansible では、serial ディレクティブを使うことで、指定した台数づつ処理を実行することができます。
Delegation, Rolling Updates, and Local Actions — Ansible DocumentationTitle

下記のようなplaybookを実行すると、PLAYレベルで分割され、sv というグループに対して1度に2台づつ、ディレクトリ作成とファイルをコピーできます。

Playbook

- name: copy file
  hosts: sv
  serial: 2
  gather_facts: false
  tasks:
    - name: Create directory
      ansible.builtin.file:
        path: ./dest
        state: directory

    - name: Copy file
      ansible.builtin.copy:
        src: ./src/file
        dest: ./dest/file

実行結果

$ anisble-playbook test.yml -i inventory.ini

### 1回目のプレイ
PLAY [sv] *****************************************************************

TASK [Create directory] ***************************************************
changed: [test1]
changed: [test2]

TASK [Copy file] **********************************************************
changed: [test1]
changed: [test2]

### 2回目のプレイ
PLAY [sv] *****************************************************************

TASK [Create directory] ***************************************************
changed: [test3]
changed: [test4]

TASK [Copy file] **********************************************************
changed: [test3]
changed: [test4]

PLAY RECAP ****************************************************************
test1 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
test2 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
test3 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
test4 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

ただ、serial を指定した状態で、他のタスクで delegate_torun_once を指定している場合は注意が必要です。

serial と run_once + delegate_to の併用の注意点

例えば、上記の Playbook に対して、コントロールノード側で git clone を実行させたい場合などです。

この場合、git clone は1度だけ実行すれば良く、かつコントロールノード側で処理させたいため、run_once + delegate_to を指定します。

すると、以下のように、serialrun_once + delegate_to を併用すると、serial で指定した台数づつ処理を実行するときに、1度だけ実行させたいタスクが、serial で分割された回数分実行されてしまいます。

Playbook

- name: delegate
  hosts: sv
  serial: 2
  gather_facts: false
  tasks:
    - name: git clone
      ansible.builtin.command:
        cmd: git clone http://nakayumc:<api>@gitlab.kensho.nakayumc.com/ansible/ansible_repo.git
      delegate_to: localhost
      run_once: true

    - name: Create directory
      ansible.builtin.file:
        path: ./dest
        state: directory

    - name: Copy file
      ansible.builtin.copy:
        src: ./src/file
        dest: ./dest/file

実行結果

$ anisble-playbook test.yml -i inventory.ini

### 1回目のプレイ
PLAY [sv] *****************************************************************

TASK [git clone] **********************************************************
changed: [test1 -> localhost]

TASK [Create directory] ***************************************************
changed: [test1]
changed: [test2]

TASK [Copy file] **********************************************************
changed: [test1]
changed: [test2]

### 2回目のプレイ
PLAY [sv] *****************************************************************

### ディレクトリが既にあるため、git clone は実行できなかった
TASK [git clone] **********************************************************
fatal: [test3 -> localhost]: FAILED! => {"changed": true, "cmd": 
["git", "clone", "http://nakayumc:<api>@gitlab.kensho.nakayumc.com/ansible_repo.git"], 
"delta": "0:00:00.002001", "end": "2023-12-23 10:32:12.488677", "msg": "non-zero 
return code", "rc": 128, "start": "2023-12-23 10:32:12.486676", "stderr": "fatal: 
destination path 'ansible_repo' already exists and is not an empty directory.", 
"stderr_lines": ["fatal: destination path 'ansible_repo' already exists and is not 
an empty directory."], "stdout": "", "stdout_lines": []}

NO MORE HOSTS LEFT ********************************************************

PLAY RECAP ****************************************************************

解決方法

解決方法としては、PLAYレベルで localhost に対して処理を実行させるもの、sv に対して処理を実行させるものに分けることで解消できました。 localhost に対して処理を実行させるものは、serial を指定せず、run_once + delegate_to を指定します。

Playbook

- name: Localhost
  hosts: localhost
  gather_facts: false

  tasks:
    - name: Git clone
      ansible.builtin.command:
        cmd: git clone http://nakayumc:<api>@gitlab.kensho.nakayumc.com/ansible/test.git
      delegate_to: localhost
      run_once: true
      ignore_errors: true

- name: Linux Hosts
  hosts: sv
  serial: 2
  gather_facts: false
  tasks:
    - name: Create directory
      ansible.builtin.file:
        path: ./dest
        state: directory

    - name: Copy file
      ansible.builtin.copy:
        src: ./src/file
        dest: ./dest/file

実行結果

$ play test.yml -i inventory.ini 

PLAY [Localhost] **********************************************************

TASK [Git clone] **********************************************************
changed: [localhost]

PLAY [Linux Hosts] ********************************************************

TASK [Create directory] ***************************************************
ok: [test1]
ok: [test2]

TASK [Copy file] **********************************************************
ok: [test2]
ok: [test1]

PLAY [Linux Hosts] ********************************************************

TASK [Create directory] ***************************************************
ok: [test4]
ok: [test3]

TASK [Copy file] **********************************************************
ok: [test4]
ok: [test3]

PLAY RECAP ****************************************************************
localhost : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0
test1     : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
test2     : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
test3     : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
test4     : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

まとめ

今回は、serial ディレクティブがある場合で、run_once + delegate_to のタスクがある場合について、
serialrun_once + delegate_to を併用すると、serial で指定した台数づつ処理を実行するときに、1度だけ実行させたいタスクが、serial で分割された回数分実行されてしまうことについて紹介しました。

serialrun_once + delegate_to を併用する場合は、PLAYレベルで localhost に対して処理を実行させるもの、sv に対して処理を実行させるものに分けることで解消できました。