Keeping files synchronized between Linux servers is a common task. You may want to copy application configuration, NGINX settings, SSL certificates, or other files from one server to another to make both look identical.
Ansible provides several ways to copy files, but for server-to-server synchronization, ansible.posix.synchronize is usually the best choice. It uses rsync, so only changed files are transferred, making it much faster than copying everything every time.
The playbook performs four steps:
- Check whether the target server already has an SSH key.
- Upload the SSH key if it is missing.
- Allow the target server to connect to the source server.
- Synchronize the required directories or files.
Prerequisites: network connection between servers, configured sudoers , generated a pair of ssh keys, defined variables used in the playbook
Step 1 – Check whether an SSH key already exists
Before uploading a key, the playbook checks whether one is already present.
- name: Check if id_rsa key already exists (target) ansible.builtin.stat: path: ~/.ssh/id_rsa register: id_rsa become: true become_user: "{{ target_host_user }}" when: ('target_host' in inventory_hostname)
The important parts are:
register: id_rsastores the result of thestatmodule so later tasks can check whether the file exists.become_user: "{{ target_host_user }}"runs the task as the application user instead ofroot. This checks the correct home directory.whenensures this task runs only on the target server.
Step 2 – Upload the SSH key
If the key does not exist, create the .ssh directory and upload both the private and public keys.
- name: Set target ssh when: ('target_host' in inventory_hostname) and (not id_rsa.stat.exists) become: true become_user: "{{ target_host_user }}" block: - name: Set target ssh | Create directories for ssh (target) ansible.builtin.file: path: "{{ item }}" state: directory owner: "{{ target_host_user }}" group: "{{ target_host_user }}" mode: "0700" loop: - ~/.ssh - name: Set target ssh | Upload keys to ssh dir no_log: true ansible.builtin.copy: dest: "~/.ssh/{{ item.file }}" content: "{{ item.content }}" mode: "{{ item.mode }}" loop: - { file: "id_rsa", content: "{{ target_host_private_key }}", mode: "0600" } - { file: "id_rsa.pub", content: "{{ target_host_public_key }}", mode: "0644" }
The interesting options are:
blockgroups related tasks under a singlewhencondition.no_log: trueprevents the private SSH key from appearing in the Ansible output or logs.loopallows us to interact with multiple files in just a one task- The task only runs when
id_rsa.stat.existsisfalse, making the playbook idempotent.
Step 3 – Allow the target server to connect
The source server must trust the target server’s SSH key.
- name: Add target public key to source host ansible.posix.authorized_key: user: "{{ source_host_user }}" key: "{{ target_host_ssh_public_key }}" state: present register: auth_key when: ('source_host' in inventory_hostname)
The authorized_key module adds the target server’s public key to the source server’s authorized_keys file. After this, the target server can connect over SSH without a password.
Step 4 – Synchronize the files
Finally, synchronize the required directories.
- name: Synchronize source and target files (target) ansible.posix.synchronize: src: "{{ source_host_user }}@{{ source_host }}:{{ item.src }}" dest: "{{ item.dest }}" mode: pull rsync_path: sudo rsync rsync_opts: - "-e 'ssh -i /home/{{ target_host_user }}/.ssh/id_rsa'" - "--mkpath" - "--delete" loop: - { src: "/etc/nginx/", dest: "/etc/nginx/" } - { src: "{{ app_dir }}/config/", dest: "{{ app_dir }}/config/" } - { src: "/home/{{ app_user }}/.somefile", dest: "/home/{{ app_user }}/" } delegate_to: "{{ inventory_hostname }}" register: rsync_result failed_when: rsync_result.rc not in [0, 23] become: true when: ('target_host' in inventory_hostname) and (auth_key is defined)
A few options are worth explaining:
mode: pullmeans the target server connects to the source server and downloads the files.src: "{{ source_host_user }}@{{ source_host }}:{{ item.src }}"tells rsync to read the files directly from the remote server over SSH.rsync_path: sudo rsyncrunsrsyncwithsudoon the source server, allowing access to protected directories such as/etc/nginx.-e 'ssh -i ...'tells rsync which SSH private key to use.--mkpathcreates the destination directory if it does not already exist.--deleteremoves files that no longer exist on the source server, keeping both servers synchronized.delegate_to: "{{ inventory_hostname }}"makes sure the synchronization runs on the target server. Since we usemode: pull, the target server is responsible for opening the SSH connection.failed_when: rsync_result.rc not in [0, 23]ignores rsync exit code 23, which commonly indicates that a source file or directory does not exist. Depending on your environment, this may be expected and should not fail the playbook.
Push vs Pull
The synchronize module supports two synchronization modes.
- Push (
mode: push) – the source server connects to the destination server and sends the files - Pull (
mode: pull) – the destination server connects to the source server and downloads the files.
This example uses pull mode because the target server initiates the SSH connection. This is often preferred when the source server should not connect to other machines.
Other ways to copy files
The synchronize module is not the only option.
copy– Copies files from the Ansible control node to managed hosts. Best for small files or configuration files.template– Similar tocopy, but processes Jinja2 templates before copying.fetch– Copies files from managed hosts back to the Ansible control node.commandorshell– Runsrsyncdirectly if you need options that are not supported by thesynchronizemodule.
I hope the post was useful.
Cheers.