Introduction

Ansible unarchive (ansible.builtin.unarchive) is the module for extracting compressed archives — .tar.gz, .zip, .tar.bz2, .tar.xz — onto a remote host. It can unpack an archive already on the control node, one already present on the remote host (remote_src: true), or download it directly from a URL and extract it in one task.

Basic Usage

# Extract local archive to remote host
- name: Extract app archive
  ansible.builtin.unarchive:
    src: files/myapp-2.1.0.tar.gz
    dest: /opt/myapp/

# Extract archive already on the remote host
- name: Extract remote archive
  ansible.builtin.unarchive:
    src: /tmp/myapp-2.1.0.tar.gz
    dest: /opt/myapp/
    remote_src: true

# Download from URL and extract
- name: Download and extract
  ansible.builtin.unarchive:
    src: https://github.com/myorg/myapp/releases/download/v2.1.0/myapp-2.1.0.tar.gz
    dest: /opt/myapp/
    remote_src: true

Parameters

ParameterDefaultDescription
src(required)Archive path or URL
dest(required)Extraction destination
remote_srcfalseArchive is on remote host (or URL)
owner—Owner of extracted files
group—Group of extracted files
mode—Permissions of extracted files
creates—Skip if this path exists (idempotency)
exclude—List of files/dirs to exclude
include—List of files/dirs to include (extract only these)
extra_opts—Extra options for tar/unzip command
keep_newerfalseDon't overwrite newer files on remote
list_filesfalseReturn list of extracted files
validate_certstrueValidate SSL when downloading URLs

Common Patterns

Download and Install Software

- name: Download Node.js
  ansible.builtin.unarchive:
    src: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz"
    dest: /usr/local/
    remote_src: true
    creates: /usr/local/node-v20.11.0-linux-x64
    extra_opts:
      - --strip-components=1    # Remove top-level directory

- name: Download and extract Go
  ansible.builtin.unarchive:
    src: https://go.dev/dl/go1.22.0.linux-amd64.tar.gz
    dest: /usr/local/
    remote_src: true
    creates: /usr/local/go/bin/go

Deploy Application Release

- name: Create release directory
  ansible.builtin.file:
    path: "/opt/myapp/releases/{{ version }}"
    state: directory
    owner: deploy
    group: deploy

- name: Extract release
  ansible.builtin.unarchive:
    src: "https://releases.example.com/myapp-{{ version }}.tar.gz"
    dest: "/opt/myapp/releases/{{ version }}"
    remote_src: true
    owner: deploy
    group: deploy
    creates: "/opt/myapp/releases/{{ version }}/bin/myapp"

- name: Switch symlink to new release
  ansible.builtin.file:
    src: "/opt/myapp/releases/{{ version }}"
    dest: /opt/myapp/current
    state: link
    force: true
  notify: Restart myapp

Extract Specific Files Only

# Extract only config files from archive
- name: Extract config only
  ansible.builtin.unarchive:
    src: /tmp/myapp-2.1.0.tar.gz
    dest: /opt/myapp/
    remote_src: true
    include:
      - "myapp-2.1.0/config/"
      - "myapp-2.1.0/config.yml"

# Exclude test files
- name: Extract without tests
  ansible.builtin.unarchive:
    src: /tmp/myapp-2.1.0.tar.gz
    dest: /opt/myapp/
    remote_src: true
    exclude:
      - "*/tests/*"
      - "*/docs/*"
      - "*.md"

Extract and List Files

- name: Extract and list contents
  ansible.builtin.unarchive:
    src: /tmp/archive.tar.gz
    dest: /opt/myapp/
    remote_src: true
    list_files: true
  register: extracted

- name: Show extracted files
  ansible.builtin.debug:
    msg: "Extracted {{ extracted.files | length }} files"

Strip Top-Level Directory

Many archives contain a top-level directory (e.g., myapp-2.1.0/). Use extra_opts to strip it:

# Archive structure: myapp-2.1.0/bin/myapp, myapp-2.1.0/config.yml
# You want: /opt/myapp/bin/myapp, /opt/myapp/config.yml

- name: Extract without top-level directory
  ansible.builtin.unarchive:
    src: /tmp/myapp-2.1.0.tar.gz
    dest: /opt/myapp/
    remote_src: true
    extra_opts:
      - --strip-components=1

Zip Files

# Extract zip (requires unzip on remote)
- name: Ensure unzip is installed
  ansible.builtin.apt:
    name: unzip
    state: present

- name: Extract zip archive
  ansible.builtin.unarchive:
    src: /tmp/webapp.zip
    dest: /var/www/html/
    remote_src: true
    owner: www-data
    group: www-data

Idempotency with creates

Without creates, the module extracts every run. Use creates to skip if already extracted:

# Only extract if /opt/myapp/bin/myapp doesn't exist
- name: Extract once
  ansible.builtin.unarchive:
    src: /tmp/myapp-2.1.0.tar.gz
    dest: /opt/myapp/
    remote_src: true
    creates: /opt/myapp/bin/myapp

Full Playbook Example

---
- name: Install Prometheus
  hosts: monitoring
  become: true
  vars:
    prometheus_version: "2.50.0"
    prometheus_url: "https://github.com/prometheus/prometheus/releases/download/v{{ prometheus_version }}/prometheus-{{ prometheus_version }}.linux-amd64.tar.gz"
  tasks:
    - name: Create prometheus user
      ansible.builtin.user:
        name: prometheus
        system: true
        shell: /usr/sbin/nologin
        create_home: false

    - name: Create directories
      ansible.builtin.file:
        path: "{{ item }}"
        state: directory
        owner: prometheus
        group: prometheus
      loop:
        - /opt/prometheus
        - /etc/prometheus
        - /var/lib/prometheus

    - name: Download and extract Prometheus
      ansible.builtin.unarchive:
        src: "{{ prometheus_url }}"
        dest: /opt/prometheus/
        remote_src: true
        owner: prometheus
        group: prometheus
        extra_opts:
          - --strip-components=1
        creates: /opt/prometheus/prometheus

    - name: Symlink binaries
      ansible.builtin.file:
        src: "/opt/prometheus/{{ item }}"
        dest: "/usr/local/bin/{{ item }}"
        state: link
      loop:
        - prometheus
        - promtool

    - name: Deploy config
      ansible.builtin.template:
        src: prometheus.yml.j2
        dest: /etc/prometheus/prometheus.yml
        owner: prometheus
      notify: Restart prometheus

    - name: Enable and start
      ansible.builtin.systemd:
        name: prometheus
        state: started
        enabled: true

  handlers:
    - name: Restart prometheus
      ansible.builtin.systemd:
        name: prometheus
        state: restarted

Troubleshooting

"Failed to find handler for" Archive

The remote host needs the right tool installed:

# For .zip files
- apt: { name: unzip, state: present }

# For .tar.xz files
- apt: { name: xz-utils, state: present }

# For .tar.bz2 files
- apt: { name: bzip2, state: present }

"Destination directory does not exist"

Create the destination first:

- file:
    path: /opt/myapp
    state: directory

- unarchive:
    src: archive.tar.gz
    dest: /opt/myapp/

Downloaded Archive Fails to Extract

Check SSL and URL:

- unarchive:
    src: https://example.com/archive.tar.gz
    dest: /opt/
    remote_src: true
    validate_certs: false    # For self-signed certs

Conclusion

ansible.builtin.unarchive extracts .tar.gz, .zip, .tar.bz2, and .tar.xz archives. Set remote_src: true to extract archives already on the remote host or download from URLs. Use creates for idempotency, extra_opts: [--strip-components=1] to remove top-level directories, and include/exclude to extract selectively. Ensure the required extraction tool (unzip, xz-utils, bzip2) is installed on the target.