Ansible の synchronize で不必要に changed になるのを防止+α

Ansible で大量のファイルをコピーしたいときは copy よりも synchronize の方が早いですが、

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/

これだけだと rsync-a が指定された状態になるので不必要に changed になることがあります。

タイムスタンプ

rsync -a はタイムスタンプも同期されるため、ファイルのタイムスタンプが異なるだけで changed になります。Git はファイルのタイムスタンプは維持しないので、ソースをチェックアウトし直しただけで changed になってしまいます。

タイムスタンプを同期しないよう times: no にしつつ、比較でタイムスタンプが無視されるように checksum: yes を付けます。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        checksum: yes
        times: no

UID/GID

rsync -a はリモート側が root で実行されていると(become: yes なり remote_user: root なり)ファイルの UID/GID も同期されるため、異なるホストで異なる UID/GID でチェックアウトしたソースだと changed になります。そのような運用は避けて Ansible を実行するための専用ホスト&アカウントを設ければ解決ではあるのですが、そうは言っても手元で実行したいこともあるでしょう。

そもそも Ansible で synchronize するときにローカルのファイルの UID/GID とターゲットホストのファイルの UID/GID を一致させたいということはあまりないでしょう(ターゲットが localhost だったり、後述のように delegate_to を使うなら別ですけど)。

ので、owner: nogroup: no を付けて UID/GID が同期されないようにします。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        checksum: yes
        times: no
        owner: no
        group: no

もしくは、rsync_opts--chown=USER:GROUP でユーザー・グループを指定します。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        checksum: yes
        times: no
        rsync_opts:
          - --chown=nobody:nobody

パーミッション

rsync -a はファイルのパーミッションも同期されます。Git は実行属性だけは維持されますがそれ以外の属性は維持されないので、チェックアウト時の umask が異なると changed になります。

ので、perms: no を付けてパーミッションが同期されないようにします。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        checksum: yes
        times: no
        owner: no
        group: no
        perms: no

もしくは、rsync_opts--chmod=CHMOD で指定します。

ファイルとディレクトリで固定の値を指定するなら次のように。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        checksum: yes
        times: no
        rsync_opts:
          - --chown=nobody:nobody
          - --chmod=D755,F644

ファイルに実行属性付きのスクリプトなどが混在しているのなら次のように。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        checksum: yes
        times: no
        rsync_opts:
          - --chown=nobody:nobody
          - --chmod=u=rwX,go=rX

むしろ archive: no にして必要なものだけ指定するほうが良いかもしれません。最低限なら次のように。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        archive: no
        recursive: yes
        checksum: yes

UID/GID やパーミッションを同期させるなら次のように。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        archive: no
        recursive: yes
        checksum: yes
        owner: yes
        group: yes
        perms: yes
        rsync_opts:
          - --chown=nobody:nobody
          - --chmod=u=rwX,go=rX

copy モジュールは mode とか owner とか group とかで指定しない限り同期されないわけなので、synchronize でも同期したいものだけ指定するほうがわかりやすいです。

use_ssh_args

本題とは関係ないのですが、synchronize には use_ssh_args というオプションがあり、これを指定すると下記などで指定された ssh のコマンドライン引数が rsyncssh にも付与されます。

  • ansible.cfg[ssh_connection] セクションの ssh_args
  • 変数 ansible_ssh_common_args
  • 変数 ansible_ssh_extra_args

Bastion ホストを中継するために ansible_ssh_common_args などで -J bastion などとしているときは use_ssh_args: yes にしておかないとこの引数が追加されないため失敗します。

- hosts: all
  become: yes
  tasks:
    - name: synchronize many files
      synchronize:
        src: many-files/
        dest: /tmp/many-files/
        archive: no
        recursive: yes
        checksum: yes
        owner: yes
        group: yes
        perms: yes
        rsync_opts:
          - --chown=nobody:nobody
          - --chmod=u=rwX,go=rX
        use_ssh_args: yes

また、ドキュメントには無いのですが ssh_args で直接コマンドライン引数を指定できます。

https://github.com/ansible/ansible/blob/v2.9.2/lib/ansible/modules/files/synchronize.py#L407

- hosts: all
  become: yes
  tasks:
    - synchronize:
        src: many-files/
        dest: /tmp/many-files/
        archive: no
        recursive: yes
        checksum: yes
        owner: yes
        group: yes
        perms: yes
        rsync_opts:
          - --chown=nobody:nobody
          - --chmod=u=rwX,go=rX
        ssh_args: -C -J bastion

use_ssh_args: yes と併用すると use_ssh_args: yes が優先されます。というか use_ssh_args: yes によってモジュールの ssh_args オプションが設定される形なので ssh_args を直接は使わないほうが良いです。

https://github.com/ansible/ansible/blob/v2.9.2/lib/ansible/plugins/action/synchronize.py#L384-L390

接続の共通は使用されない

本題とは関係ないのですが、synchronize では ssh のコマンドラインに -S none が固定で追加されるため ControlMaster による接続の共有は使用されず、毎回新規に接続されます。

https://github.com/ansible/ansible/blob/v2.9.2/lib/ansible/modules/files/synchronize.py#L523 ssh_cmd = [module.get_bin_path('ssh', required=True), '-S', 'none']

これは下記の問題に対応するために追加されたようです。

ControlPersist sockets conflict with synchronize and rsync commands in playbook · Issue #8473 · ansible/ansible

プレイブックを実行する sshrsyncssh が競合するため? ControlMaster の接続の共有は1本の接続を複数の ssh で共有できるので大丈夫そうですけど・・なにが問題なのかは良くわかりませんでした。

delegate_to

本題とは関係ないのですが、synchronize モジュールは delegate_to を指定したときの動きが他のモジュールと異なります。

synchronize モジュールは、要するにローカルホスト上でローカルファイルをソースとしてターゲットホストを宛先に rsync するものですが、delegate_to を指定すると rsync を実行するローカルホストを別のホストに変更することができます。

例えば、copy だと delegate_to はコピーする先のターゲットホストになりますが、synchronize だと delegate_to はコピーする元のソースホストになります。つまり、delegate_to で指定したホストから本来のターゲットホストへの rsync になります。

- hosts: are
  become: yes
  tasks:
    - synchronize:
        src: /tmp/many-files/
        dest: /tmp/many-files/
      delegate_to: ore

この例では ore から arersync されます。もちろん ore から are への ssh を通す必要があります。ansible.cfgssh_connection.ssh_args-A を追加するなどしてエージェントフォワーディングを有効にするなどが必要です。