copy ModuleIn multi-node infrastructure management, distributing static files, application artifacts, and configuration scripts from an Ansible Control Node to target managed nodes is a core operational requirement. Ansible provides the built-in ansible.builtin.copy module to manage file transfers seamlessly over existing SSH connections.
Unlike traditional file transfer utilities like scp or rsync which re-transfer files regardless of state, the Ansible copy module operates on the principle of Idempotency. Before transferring data, Ansible calculates cryptographic SHA-1 / MD5 checksums of the source file on the control node and compares them against the destination file on each remote node. If the remote file exists and its checksum matches the source file, Ansible skips the transfer (changed: false), saving bandwidth and preventing unnecessary disk I/O.
graph TD
subgraph ControlNode ["Jump Host (Ansible Control Node)"]
ThorUser["User: thor"]
InvFile["Inventory File<br/>/home/thor/ansible/inventory"]
SrcFile["Source File<br/>/usr/src/data/index.html"]
AnsibleCLI["Ansible Engine<br/>ansible-playbook"]
end
subgraph StratosDC ["Stratos Datacenter (Managed Nodes)"]
subgraph AppServers ["Group: [app_servers]"]
App1["stapp01<br/>IP: 172.16.238.10"]
App2["stapp02<br/>IP: 172.16.238.11"]
App3["stapp03<br/>IP: 172.16.238.12"]
end
DestPath1["Dest: /opt/data/index.html"]
DestPath2["Dest: /opt/data/index.html"]
DestPath3["Dest: /opt/data/index.html"]
end
SrcFile -->|"1. Calculate Local Checksum"| AnsibleCLI
InvFile -->|"2. Target Hosts & Auth"| AnsibleCLI
AnsibleCLI -->|"3. Compare Remote Checksum via SSH"| App1
AnsibleCLI -->|"3. Compare Remote Checksum via SSH"| App2
AnsibleCLI -->|"3. Compare Remote Checksum via SSH"| App3
App1 -->|"4. Push File if Checksum Mismatches"| DestPath1
App2 -->|"4. Push File if Checksum Mismatches"| DestPath2
App3 -->|"4. Push File if Checksum Mismatches"| DestPath3
copy ModuleThe ansible.builtin.copy module accepts key parameters to control transfer behavior, file permissions, and ownership:
src (Source Path):
src is a directory ending with / (e.g., /src/dir/), Ansible copies the contents of the directory into the destination. If src has no trailing slash (e.g., /src/dir), Ansible copies the directory itself into the destination.dest (Destination Path):
dest ends with a trailing slash /, the file retains its source filename inside the target directory.mode (Permissions):
'0644', '0755'). Always quote octal mode strings to prevent YAML parser misinterpretation.owner & group (Ownership):
become: yes if target user differs from execution user).backup (Backup Creation):
yes, Ansible creates a timestamped backup copy of the remote file before overwriting it.content (Inline String Creation):
src. Allows passing inline string literals directly into a remote file without storing a local source file.| Feature | ansible.builtin.copy |
ansible.builtin.template |
|---|---|---|
| Primary Use Case | Distributing static files, binaries, images, compiled artifacts | Generating dynamic configuration files (nginx.conf, httpd.conf) |
| Processing Engine | Raw file byte transfer (SHA-1 checksum validation) | Jinja2 templating engine (``) |
| Variable Substitution | No variable evaluation inside the file | Evaluates dynamic facts, inventory variables, and loops |
| Host Role | Hostname / Alias | IP Address | SSH User | Source File Path | Target File Path |
|---|---|---|---|---|---|
| Control Node | jump_host |
172.16.238.2 |
thor |
/usr/src/data/index.html |
N/A |
| App Server 1 | stapp01 |
172.16.238.10 |
tony |
N/A | /opt/data/index.html |
| App Server 2 | stapp02 |
172.16.238.11 |
steve |
N/A | /opt/data/index.html |
| App Server 3 | stapp03 |
172.16.238.12 |
banner |
N/A | /opt/data/index.html |
/home/thor/ansible/inventory/home/thor/ansible/playbook.yml/usr/src/data/index.html on jump_host/opt/data/ on all target App Servers (stapp01, stapp02, stapp03)ansible.builtin.copyansible-playbook -i inventory playbook.ymlSSH into the Jump Host as user thor:
ssh thor@jump_host
Verify that the source file exists on the Jump Host:
ls -l /usr/src/data/index.html
Navigate to the Ansible project directory:
cd /home/thor/ansible
Ensure /home/thor/ansible/inventory contains all target application servers under group [app_servers]:
[app_servers]
stapp01 ansible_host=stapp01 ansible_user=tony ansible_ssh_pass=******
stapp02 ansible_host=stapp02 ansible_user=steve ansible_ssh_pass=******
stapp03 ansible_host=stapp03 ansible_user=banner ansible_ssh_pass=******
Verify SSH connectivity using an ad-hoc ping:
ansible app_servers -i inventory -m ping
Create /home/thor/ansible/playbook.yml using vi or nano:
vi /home/thor/ansible/playbook.yml
Add the following YAML declaration:
---
- name: Copy data file to all App Servers
hosts: app_servers
become: yes
tasks:
- name: Copy /usr/src/data/index.html to /opt/data/
ansible.builtin.copy:
src: /usr/src/data/index.html
dest: /opt/data/index.html
mode: '0644'
Validate the playbook syntax before running:
ansible-playbook -i inventory playbook.yml --syntax-check
Expected Output:
playbook: playbook.yml
Run the playbook against the inventory file:
ansible-playbook -i inventory playbook.yml
Expected Output:
PLAY [Copy data file to all App Servers] *********************************************************
TASK [Gathering Facts] ****************************************************************************
ok: [stapp01]
ok: [stapp02]
ok: [stapp03]
TASK [Copy /usr/src/data/index.html to /opt/data/] ************************************************
changed: [stapp01]
changed: [stapp02]
changed: [stapp03]
PLAY RECAP ****************************************************************************************
stapp01 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
stapp02 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
stapp03 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Run an ad-hoc shell command to verify that index.html exists in /opt/data/ across all three app servers:
ansible app_servers -i inventory -m shell -a "ls -l /opt/data/index.html"
Expected Output:
stapp01 | CHANGED | rc=0 >>
-rw-r--r-- 1 root root 120 Aug 6 18:49 /opt/data/index.html
stapp02 | CHANGED | rc=0 >>
-rw-r--r-- 1 root root 120 Aug 6 18:49 /opt/data/index.html
stapp03 | CHANGED | rc=0 >>
-rw-r--r-- 1 root root 120 Aug 6 18:49 /opt/data/index.html
Re-run the playbook execution command to verify that Ansible reports changed=0 on all hosts:
ansible-playbook -i inventory playbook.yml
Expected Output:
PLAY RECAP ****************************************************************************************
stapp01 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
stapp02 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
stapp03 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ansible.builtin.copy checksum comparison instead of running shell scripts (scp/rsync).mode parameter explicitly (e.g. mode: '0644') to avoid inheriting unwanted execution bits from the control node.ansible.builtin.file with state: directory in a preceding task or ensure privileges allow directory creation.backup: yes when overwriting existing production files to allow instant rollback if needed.