Ansible Copy Examples - How to copy files and directories with Ansible

In this article, we are going to see practical examples of Ansible copy. We will be seeing various examples of ansible copy

To keep things simple we have created the playbook that you can test in your localhost itself, In other words, ansible control machine

you can change the hosts variable to a valid remote host group if you would like to try on the remote machines.

Ansible Copy

Ansible Copy Module

The copy module executes a simple copy on the file or directory on the local or on the remote machine.

You can use an ansible copy for the following requirements

  • To copy files from a local source to a local destination
  • To copy files from a remote source to a remote destination (remote_src)
  • To copy files from a local source to a remote destination

You can use the fetch module to copy files from the remote source to local on the other hand.

We will be seeing various examples of Ansible copy in this article. we have one more article on Ansible copy for you to read and explore.

Quick Syntax of Ansible Copy

 

# copy_file.yml

- name: copy files to destination
  hosts: localhost
  connection: local
  tasks:
    - name: copy src.txt as dest.txt in the same dir 
      copy:
        src: files/src.txt
        dest: files/dest.txt
      tags:
        - simple_copy

 

The preceding playbook consists of a single play. The assumptions to execute the above playbook are,

  1. There exists a files directory in the same location as the playbook
  2. There exists a file src.txt inside the above files directory.

The play consists of a task that uses the copy module to copy the “src” to its “dest”.

By default, the ansible copy module does a force copy to the destination and overwrites the existing file when present.

Just copy the above playbook and run it in localhost by following the below commands in order

sseshadr@SSESHADR-M-24FK copy_module % mkdir -p files
sseshadr@SSESHADR-M-24FK copy_module % echo "adding a new line to files/src.txt" >> files/src.txt 
sseshadr@SSESHADR-M-24FK copy_module % 
sseshadr@SSESHADR-M-24FK copy_module % ansible-playbook copy_file.yml – tags "simple_copy" -v    
No config file found; using defaults
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [copy files to destination] *****************************************************************************************************************

TASK [Gathering Facts] ***************************************************************************************************************************
ok: [localhost]

TASK [force copy src.txt as dest.txt in the same dir] ********************************************************************************************
changed: [localhost] => {"changed": true, "checksum": "f292ebaaaa1651f4875e13ea6346b48b9f817421", "dest": "files/dest.txt", "gid": 20, "group": "staff", "md5sum": "85cfe9b0250baed542831184c0a4c4d5", "mode": "0644", "owner": "sseshadr", "size": 65, "src": "/Users/sseshadr/.ansible/tmp/ansible-tmp-1641913099.0320861-74874-262311856787358/source", "state": "file", "uid": 501}

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

sseshadr@SSESHADR-M-24FK copy_module % ls -l files/dest.txt 
-rw-r--r –  1 sseshadr  staff  65 Jan 11 20:28 files/dest.txt

The file src.txt was copied successfully as “files/dest.txt” as we can see from the output of the copy task, changed: true

 

How to disable Force Copy of Ansible Copy

If the force copy has to be disabled, i.e., ignore the copy task if the file is already present, then use the option force: no in the copy-module as shown below.

# copy_file.yml

- name: copy files to destination
  hosts: localhost
  connection: local
  tasks:
    - name: no force copy src.txt as dest.txt in the same dir 
      copy:
        src: files/src.txt
        dest: files/dest.txt
        force: no
      tags:
        - simple_copy_no_force

 

The output of the preceding playbook is given below

sseshadr@SSESHADR-M-24FK copy_module % ls -l files/dest.txt 
-rw-r--r –  1 sseshadr  staff  65 Jan 11 20:28 files/dest.txt
sseshadr@SSESHADR-M-24FK copy_module % 
sseshadr@SSESHADR-M-24FK copy_module % date
Tue Jan 11 20:35:31 IST 2022
sseshadr@SSESHADR-M-24FK copy_module % 
sseshadr@SSESHADR-M-24FK copy_module % ansible-playbook copy_file.yml – tags "simple_copy_no_force" -v
No config file found; using defaults
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [copy files to destination] *****************************************************************************************************************

TASK [Gathering Facts] ***************************************************************************************************************************
ok: [localhost]

TASK [dont force copy src.txt as dest.txt in the same dir] ***************************************************************************************
ok: [localhost] => {"changed": false, "dest": "files/dest.txt", "src": "/Users/sseshadr/ansible-test/module_test/copy_module/files/src.txt"}

PLAY RECAP ***************************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

sseshadr@SSESHADR-M-24FK copy_module % 
sseshadr@SSESHADR-M-24FK copy_module % ls -l files/dest.txt                                           
-rw-r--r –  1 sseshadr  staff  65 Jan 11 20:28 files/dest.txt

We can see that the last modified time of the dest.txt is unchanged and the file was not overwritten by src.txt even though it had a new line since we had disabled force copy in our playbook.

Overwrite and backup the original file

What if the copied file contains a few mistakes but the copy module had overwritten the previous version? No worries, we have the option in the ansible copy module to take a backup of the previous version of the destination file. So it's now easy to revert the copy.

# copy_file.yml

- name: copy files to destination
  hosts: localhost
  connection: local
  tasks:
    - name: copy src.txt to files/backup_test and create a backup of src.txt
      copy:
        src: files/src.txt
        dest: files/backup_test/
        backup: yes 
      tags:
        - backup

 

In this example, we would be copying files/src.txt to files/backup_test directory.

The destination filename would also be src.txt.

To demonstrate this example, the copy was already run once and we would be re-running the same play by adding an extra line to the source file. Only then we would be getting a backup of the original destination file.

sseshadr@SSESHADR-M-24FK copy_module % echo "creating a new line in src.txt" >> files/src.txt
sseshadr@SSESHADR-M-24FK copy_module %                                                       
sseshadr@SSESHADR-M-24FK copy_module % ansible-playbook copy_file.yml – tags "backup" -v     
No config file found; using defaults
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [copy files to destination] *****************************************************************************************************************

TASK [Gathering Facts] ***************************************************************************************************************************
ok: [localhost]

TASK [copy src.txt to files/backup_test and create a backup of src.txt] **************************************************************************
changed: [localhost] => {"backup_file": "files/backup_test/src.txt.77306.2022-01-11@20:45:03~", "changed": true, "checksum": "e2a8f9574b6e1b7e39a0c8abdff02b7edc076d28", "dest": "files/backup_test/src.txt", "gid": 20, "group": "staff", "md5sum": "5a4cf003863bec14f92c132143e6d217", "mode": "0644", "owner": "sseshadr", "size": 242, "src": "/Users/sseshadr/.ansible/tmp/ansible-tmp-1641914102.470293-77281-75182788946002/source", "state": "file", "uid": 501}

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

sseshadr@SSESHADR-M-24FK copy_module % ls -l files/backup_test                               
total 16
-rw-r--r –  1 sseshadr  staff  242 Jan 11 20:45 src.txt
-rw-r--r –  1 sseshadr  staff  211 Jan 11 20:44 src.txt.77306.2022-01-11@20:45:03~

As we can see the latest files/src.txt got copied as files/backup_test/src.txt and a backup of the previous version was also created as “src.txt.77306.2022-01-11@20:45:03~”. We can use this file to revert back to the previous version if something nasty happened due to the copy.

Copying file to a non-existing directory

If the destination directory does not exist, the copy module takes care of creating it and copying the file to the new directory with the same name as the source file name.

# copy_file.yml

- name: copy files to destination
  hosts: localhost
  connection: local
  tasks:
    - name: copy src.txt to a non existing directory
      copy:
        src: files/src.txt
        dest: files/not_dir/
      tags:
        - dir_not_exist

Output of the preceding playbook is given below.

sseshadr@SSESHADR-M-24FK copy_module % ls files/not_dir
ls: files/not_dir: No such file or directory
sseshadr@SSESHADR-M-24FK copy_module % ansible-playbook copy_file.yml – tags "dir_not_exist" -v 
No config file found; using defaults
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [copy files to destination] *****************************************************************************************************************

TASK [Gathering Facts] ***************************************************************************************************************************
ok: [localhost]

TASK [copy src.txt to a non existing directory] **************************************************************************************************
changed: [localhost] => {"changed": true, "checksum": "e2a8f9574b6e1b7e39a0c8abdff02b7edc076d28", "dest": "files/not_dir/src.txt", "gid": 20, "group": "staff", "md5sum": "5a4cf003863bec14f92c132143e6d217", "mode": "0644", "owner": "sseshadr", "size": 242, "src": "/Users/sseshadr/.ansible/tmp/ansible-tmp-1641915480.656468-80209-40624732707130/source", "state": "file", "uid": 501}

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

sseshadr@SSESHADR-M-24FK copy_module % ls files/not_dir 
src.txt

Copy entire directory

# copy_dir.yml

- name: copy module for directories
  hosts: localhost
  connection: local
  tasks:
    - name: copy dir1 to /tmp
      copy:
        src: dir1
        dest: /tmp/
        directory_mode:
      tags:
        - parentdir

 

The preceding play assumes that there is a directory dir1 in the same location as the playbook. On running the above play, the entire directory dir1 and its contents will be recursively copied to the destination /tmp as /tmp/dir1

sseshadr@SSESHADR-M-24FK copy_module % mkdir dir1 && mkdir -p dir1/inner_dir1 && touch dir1/outer_file && touch dir1/inner_dir1/inner_file

sseshadr@SSESHADR-M-24FK copy_module % ls -Rl dir1 
total 0
drwxr-xr-x  3 sseshadr  staff  96 Jan 11 21:14 inner_dir1
-rw-r--r –  1 sseshadr  staff   0 Jan 11 21:14 outer_file

dir1/inner_dir1:
total 0
-rw-r--r –  1 sseshadr  staff  0 Jan 11 21:14 inner_file

sseshadr@SSESHADR-M-24FK copy_module % ansible-playbook copy_dir.yml – tags "parentdir" -v
No config file found; using defaults
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [copy module for directories] ***************************************************************************************************************

TASK [Gathering Facts] ***************************************************************************************************************************
ok: [localhost]

TASK [copy dir1 to /tmp] *************************************************************************************************************************
changed: [localhost] => {"changed": true, "dest": "/tmp/", "src": "/Users/sseshadr/ansible-test/module_test/copy_module/dir1"}

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

sseshadr@SSESHADR-M-24FK copy_module % ls -Rl /tmp/dir1 
total 0
drwxr-xr-x  3 sseshadr  wheel  96 Jan 11 21:14 inner_dir1
-rw-r--r –  1 sseshadr  staff   0 Jan 11 21:14 outer_file

/tmp/dir1/inner_dir1:
total 0
-rw-r--r –  1 sseshadr  staff  0 Jan 11 21:14 inner_file

As we can see from the above output, the entire directory was copied recursively to the destination.

Ansible Copy only directory contents

- name: copy module for directories
  hosts: localhost
  connection: local
  tasks:    
    - name: copy contents of dir1 to /tmp/dir1_contents
      copy:
        src: dir1/
        dest: /tmp/dir1_contents/
        directory_mode:
      tags:
        - dircontent

 

If only the contents of the directory need to be copied leaving out the outer directory, then src should end with a forward slash / which represents the contents of the src directory (in our case, contents of dir1)

sseshadr@SSESHADR-M-24FK copy_module % ansible-playbook copy_dir.yml – tags "dircontent" -v 
No config file found; using defaults
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [copy module for directories] ***************************************************************************************************************

TASK [Gathering Facts] ***************************************************************************************************************************
ok: [localhost]

TASK [copy contents of dir1 to /tmp/dir1_contents] ***********************************************************************************************
changed: [localhost] => {"changed": true, "dest": "/tmp/dir1_contents/", "src": "/Users/sseshadr/ansible-test/module_test/copy_module/dir1/"}

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

sseshadr@SSESHADR-M-24FK copy_module % ls -Rl /tmp/dir1_contents 
total 0
drwxr-xr-x  3 sseshadr  wheel  96 Jan 11 21:20 inner_dir1
-rw-r--r –  1 sseshadr  staff   0 Jan 11 21:20 outer_file

/tmp/dir1_contents/inner_dir1:
total 0
-rw-r--r –  1 sseshadr  staff  0 Jan 11 21:20 inner_file

We can see that the outer directory dir1 was left and only its contents were copied to /tmp/dir1_contents

Ansible Copy raw content to file

Not just files, ansible copy module can also write content to a destination file.

This feature of the ansible copy module is very useful if we want to dump some string content or “Ansible variables” into a file (say a dict).

Instead of the src option, we have to use the content argument and pass the necessary content to be written as the destination file.

#copy_file.yml

- name: copy files to destination
  hosts: localhost
  connection: local
  vars:
    somedict:
      key1: value1
      key2: value2
  tasks:
    - name: copy content to content_dest.txt
      copy:
        content: |
          Hello from ansible.
          This is a sample file.
          This is a sample dict,
          {{ somedict }}
        dest: files/content_dest.txt
      tags:
        - content

 

sseshadr@SSESHADR-M-24FK copy_module % ansible-playbook copy_file.yml – tags "content" -v 
No config file found; using defaults
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [copy files to destination] *****************************************************************************************************************

TASK [Gathering Facts] ***************************************************************************************************************************
ok: [localhost]

TASK [copy content to content_dest.txt] **********************************************************************************************************
changed: [localhost] => {"changed": true, "checksum": "783f59cb4d9068edf70494178913ff64426d2bea", "dest": "files/content_dest.txt", "gid": 20, "group": "staff", "md5sum": "fb94495fdebe48ba727186eddc14d5af", "mode": "0644", "owner": "sseshadr", "size": 103, "src": "/Users/sseshadr/.ansible/tmp/ansible-tmp-1641916547.4278688-82599-191762476043983/source", "state": "file", "uid": 501}

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

sseshadr@SSESHADR-M-24FK copy_module % cat files/content_dest.txt 
Hello from ansible.
This is a sample file.
This is a sample dict,
{'key1': 'value1', 'key2': 'value2'}

How to Verify if the copy is successful

So far we have trusted the copy-module to have successfully copied our source file to the destination.

What if we would like to know if the copy was TRULY successful indeed. Normally calculating the checksum of a file is a good technique to verify if two files are identical and there is no loss or corruption of data during the copy.

So we will look into the ways to verify the result of the copy-module by comparing the checksums of the source and destination file.

- name: copy files to destination
  hosts: localhost
  connection: local
  tasks:
    - block:
      - name: get properties of src.txt
        stat:
          path: files/src.txt
          checksum_algorithm: sha1
        register: src_info

      - name: copy src.txt to dest.txt
        copy:
          src: files/src.txt
          dest: files/dest.txt
          force: yes
          checksum:
        register: copy_out

      - name: Fail if copy was a failure
        fail:
          msg: "Copy failed!"
        when: src_info.stat.checksum != copy_out.checksum

      - name: Print Copy successful
        debug:
          msg: "Copy Successful!"

      tags:
        - checksum

 

The stat module can be used to find the initial checksum of the source file before copying. After copying, the copy-module itself returns a dict that contains the checksum of the destination file (SHA1 checksum).

Since the copy-module returns a “SHA1 checksum”, we can also calculate the “SHA1 checksum” of the source file by passing it as an argument to the stat module.

In the end, both the checksum values are compared to see if they are equal.

sseshadr@SSESHADR-M-24FK copy_module % ansible-playbook copy_file.yml – tags "checksum" -v
No config file found; using defaults
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [copy files to destination] *****************************************************************************************************************

TASK [Gathering Facts] ***************************************************************************************************************************
ok: [localhost]

TASK [get properties of src.txt] *****************************************************************************************************************
ok: [localhost] => {"changed": false, "stat": {"atime": 1641914093.9985154, "attr_flags": "", "attributes": [], "birthtime": 1641913076.4996405, "block_size": 4096, "blocks": 8, "charset": "us-ascii", "checksum": "e2a8f9574b6e1b7e39a0c8abdff02b7edc076d28", "ctime": 1641914092.7721033, "dev": 16777220, "device_type": 0, "executable": false, "exists": true, "flags": 0, "generation": 0, "gid": 20, "gr_name": "staff", "inode": 13512847, "isblk": false, "ischr": false, "isdir": false, "isfifo": false, "isgid": false, "islnk": false, "isreg": true, "issock": false, "isuid": false, "mimetype": "text/plain", "mode": "0644", "mtime": 1641914092.7721033, "nlink": 1, "path": "files/src.txt", "pw_name": "sseshadr", "readable": true, "rgrp": true, "roth": true, "rusr": true, "size": 242, "uid": 501, "version": null, "wgrp": false, "woth": false, "writeable": true, "wusr": true, "xgrp": false, "xoth": false, "xusr": false}}

TASK [copy src.txt to dest.txt] ******************************************************************************************************************
ok: [localhost] => {"changed": false, "checksum": "e2a8f9574b6e1b7e39a0c8abdff02b7edc076d28", "dest": "files/dest.txt", "gid": 20, "group": "staff", "mode": "0644", "owner": "sseshadr", "path": "files/dest.txt", "size": 242, "state": "file", "uid": 501}

TASK [Fail if copy was a failure] ****************************************************************************************************************
skipping: [localhost] => {"changed": false, "skip_reason": "Conditional result was False"}

TASK [Print Copy successful] *********************************************************************************************************************
ok: [localhost] => {
    "msg": "Copy Successful!"
}

PLAY RECAP ***************************************************************************************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

The copy was successful as the checksums values are the same before and after copy!

Cheers
Sudarshan Seshadri