zaki work log

作業ログやら生活ログやらなんやら

[Ansible] fetchモジュールを使ってリモートのファイルを実行ノードへ転送・集約する

最近アウトプットできてなかったけど、これはAnsible Advent Calendar 2021の13日目のエントリです。

adventar.org

個人的にあまりなじみがないfetchモジュールについての備忘録です。
このモジュールはcopyモジュールの逆で、マネージドノード上のファイルをコントロールノードへコピーします。システム構築の自動化でAnsaibleを使うようなシチュエーションだと割と使用頻度が低い(個人の感想)のですが、サービスの日々の運用で、各ホスト上の例えばログファイルを1か所へ集約したい場合に本領を発揮します(当社比)。

docs.ansible.com

基本の使い方

  tasks:
    - name: fetch file
      ansible.builtin.fetch:
        src: /var/log/messages
        dest: /var/tmp/fetch-test

モジュールの基本機能は、リモートホストsrcに指定したファイルを、Ansible実行ホストのdestで指定したディレクトリ以下にリモートホスト名/リモート上のフルパスというパスでコピーする。
このとき、destディレクトリが無くても自動で作成される。

flatパラメタ

コピー時のリモートのパスやリモートホスト名が不要で、destに指定したパスに直接集めたい場合は、flatパラメタを指定する。

    - name: fetch file
      ansible.builtin.fetch:
        src: /var/log/messages
        dest: /var/tmp/fetch-test
        flat: true

flatパラメタをtrueにすると、destに指定したパスが転送先になる。
この例だと、リモートの/var/log/messagesファイルは/var/tmp/fetch-testというファイル名で転送される。

また、

    - name: fetch file
      ansible.builtin.fetch:
        src: /var/log/messages
        dest: /var/tmp/fetch-test/  # '/' 追加
        flat: true

このようにdestの指定をディレクトリ(末尾/)にすると、ファイル名は維持され転送先は/var/tmp/fetch-test/messagesになる。

ただしflatを使う場合、処理対象のホストが複数ありファイル名が同一だとすべて同じファイルで転送されるため、処理順が最後のホスト以外のファイルは上書きされてしまうので注意。
flatを使用しつつ、ファイル名にホスト名を付与したいのであれば、マジック変数のinventory_hostnameを使うなどしてパスやファイル名がホスト毎になるように調整する。

        dest: "/var/tmp/fetch-test/{{ inventory_hostname }}/"

docs.ansible.com

fail_on_missingパラメタ

デフォルトの動作は、リモートホスト上にsrcで指定したファイルが無い場合は失敗となる。
要件によって「ファイルが無い場合は何もしない(正常完了)」したい場合は、fail_on_missingパラメタにfalseを指定する。
(ファイルが存在しない場合以外の転送失敗なども軒並み無視するignore_errorsなどを使わないよーに)

    - name: fetch file
      ansible.builtin.fetch:
        src: /var/log/messages
        dest: /var/tmp/fetch-test
        fail_on_missing: false

このオプションは、古いAnsibleバージョンだとデフォルト値が逆になっているので(バージョン塩漬けのような)環境によっては動作が異なるので注意。

ディレクトリ以下を再帰的にコピーするには…

fetchモジュールには再帰コピーの機能はない1ため、自力で処理する必要がある。
ファイルリストを作成するにはfindモジュール辺りを使ったタスクを別途用意する。

    - name: create file list
      ansible.builtin.find:
        paths: /var/log
        recurse: true
      register: register_file_stat

    - name: fetch files
      ansible.builtin.fetch:
        src: "{{ item }}"
        dest: /var/tmp/fetch-test
      loop: "{{ register_file_stat.files | map(attribute='path') }}"

ただしこのとき、ファイル数やサイズが大量で転送に時間がかかる場合でかつ、リモートホスト上のファイルの作成や削除が頻繁に発生するような環境の場合、リスト作成時点から転送までのタイムラグによってファイルの有無が変化すると転送失敗になったりするので注意が必要。

対象ディレクトリをarchiveモジュールでファイルに固めて転送する方が良い場合もあるので、要件によって検討する。
手順は増えるが、転送後のアーカイブファイルの展開と削除も必要に応じて行う。

    - name: archive files
      community.general.archive:
        path: /var/log
        dest: /tmp/log.tar.gz

    - name: fetch file
      ansible.builtin.fetch:
        src: /tmp/log.tar.gz
        dest: /var/tmp/fetch-test

ちなみにarchiveモジュールは普段使わないから気付かなかったけど、アーカイブ対象に変化が無かったら実行ステータスはokになって冪等性が担保されるように見えるけど、その場合でもアーカイブファイルのタイムスタンプが更新されてしまうため、その後のタスクの冪等性を担保するのはすこし難しいかも。
(ファイルの更新は最新のcommunity.general 4.1.0でも再現)

ただ、「定期実行でそのタイミングのスナップショット的にファイル構造を丸ごと保存したい」みたいな要件の場合は、アーカイブ式の方が扱いやすいかも。

環境

$ ansible --version
ansible [core 2.11.3] 
  config file = /home/zaki/.ansible.cfg
  configured module search path = ['/home/zaki/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/zaki/venv/ansible4.2.0/lib64/python3.9/site-packages/ansible
  ansible collection location = /home/zaki/.ansible/collections:/usr/share/ansible/collections
  executable location = /home/zaki/venv/ansible4.2.0/bin/ansible
  python version = 3.9.2 (default, Mar  5 2021, 01:49:45) [GCC 8.4.1 20200928 (Red Hat 8.4.1-1)]
  jinja version = 3.0.1
  libyaml = True


  1. 現状のモジュールのドキュメントに「Recursive fetching may be supported in a later release.」とそのうち実装されるかも的な記述はあるけど。