Introduction

Backing up Windows systems with Ansible automates what's often a manual, error-prone process. Whether you're protecting Windows 10/11 workstations, Windows Server 2019/2022, or entire fleets, Ansible modules like ansible.windows.win_copy, community.windows.win_robocopy, and ansible.windows.win_shell give you repeatable, idempotent backup automation. This guide covers file-level backups, system state, database dumps, and scheduled backup tasks.

Prerequisites

# Requirements:
# - WinRM configured on target hosts (or SSH for Windows)
# - ansible.windows collection installed
# - community.windows collection installed (for win_robocopy)

# Install required collections
# ansible-galaxy collection install ansible.windows community.windows

Backup Methods Comparison

Method              | Best For           | Module                | Speed
────────────────────|────────────────────|───────────────────────|──────
win_robocopy        | Large folder sync  | community.windows     | Fast
win_copy            | Small files/config | ansible.windows       | Medium
win_shell + robocopy| Custom robocopy    | ansible.windows       | Fast
fetch               | Pull to controller | ansible.builtin       | Slow
VSS + wbadmin       | System state       | ansible.windows       | Slow

Enterprise Backup Scenario

For an enterprise backup playbook, keep Windows hosts in a dedicated inventory group and use this workflow for file shares, IIS data, registry exports, SQL Server dumps, and scheduled tasks. If the same backup policy also covers Linux hosts, pair this Windows playbook with a separate Linux file backup role or a general Ansible backup and restore guide so each operating system uses the right modules while sharing retention, verification, and destination naming rules.

Basic File Backup with win_copy

# backup-files.yml
---
- name: Backup Windows files to network share
  hosts: windows_hosts
  vars:
    backup_date: "{{ ansible_date_time.date }}"
    backup_dest: "\\\\backup-server\\backups\\{{ inventory_hostname }}\\{{ backup_date }}"

  tasks:
    - name: Create backup directory
      ansible.windows.win_file:
        path: "{{ backup_dest }}"
        state: directory

    - name: Backup application config files
      ansible.windows.win_copy:
        src: "C:\\App\\config\\"
        dest: "{{ backup_dest }}\\config\\"
        remote_src: true

    - name: Backup IIS configuration
      ansible.windows.win_copy:
        src: "C:\\Windows\\System32\\inetsrv\\config\\applicationHost.config"
        dest: "{{ backup_dest }}\\iis-config\\"
        remote_src: true

Folder Backup with win_robocopy

win_robocopy is the best choice for large directory backups — it's fast, supports mirroring, and handles retries.

# backup-robocopy.yml
---
- name: Backup Windows folders with Robocopy
  hosts: windows_hosts
  vars:
    backup_source: "C:\\ImportantData"
    backup_dest: "D:\\Backups\\{{ inventory_hostname }}\\{{ ansible_date_time.date }}"

  tasks:
    - name: Mirror data folder to backup location
      community.windows.win_robocopy:
        src: "{{ backup_source }}"
        dest: "{{ backup_dest }}"
        recurse: true
        flags:
          - /MIR        # Mirror (exact copy, deletes extras)
          - /Z          # Restartable mode (network resilience)
          - /MT:16      # 16 threads for speed
          - /R:3        # Retry 3 times on failure
          - /W:5        # Wait 5 seconds between retries
          - /NFL        # No file list in output (cleaner logs)
          - /NDL        # No directory list in output
          - /NP         # No progress percentage
      register: robocopy_result

    - name: Show backup summary
      ansible.builtin.debug:
        msg: >-
          Backup complete:
          {{ robocopy_result.msg | default('OK') }}

Complete Backup Playbook

# full-backup-windows.yml
---
- name: Complete Windows backup
  hosts: windows_hosts
  vars:
    backup_root: "\\\\nas01\\backups"
    backup_dir: "{{ backup_root }}\\{{ inventory_hostname }}\\{{ ansible_date_time.date }}"
    retention_days: 30
    backup_sources:
      - name: "user-data"
        src: "C:\\Users\\Public\\Documents"
      - name: "app-config"
        src: "C:\\ProgramData\\MyApp"
      - name: "iis-sites"
        src: "C:\\inetpub\\wwwroot"

  tasks:
    # --- Pre-backup checks ---
    - name: Verify backup share is accessible
      ansible.windows.win_shell: |
        if (Test-Path "{{ backup_root }}") { "OK" } else { throw "Backup share not accessible" }
      register: share_check
      changed_when: false

    - name: Create dated backup directory
      ansible.windows.win_file:
        path: "{{ backup_dir }}"
        state: directory

    # --- File backups ---
    - name: Backup each source directory
      community.windows.win_robocopy:
        src: "{{ item.src }}"
        dest: "{{ backup_dir }}\\{{ item.name }}"
        recurse: true
        flags:
          - /MIR
          - /Z
          - /MT:8
          - /R:3
          - /W:10
          - /XO         # Exclude older files (only copy newer)
      loop: "{{ backup_sources }}"
      loop_control:
        label: "{{ item.name }}"
      register: backup_results

    # --- Registry backup ---
    - name: Export critical registry keys
      ansible.windows.win_shell: |
        reg export "HKLM\SOFTWARE\MyApp" "{{ backup_dir }}\registry-myapp.reg" /y
      args:
        creates: "{{ backup_dir }}\\registry-myapp.reg"

    # --- Windows System State ---
    - name: Backup Windows System State (servers only)
      ansible.windows.win_shell: |
        wbadmin start systemstatebackup -backupTarget:{{ backup_dir }}\systemstate -quiet
      when: ansible_os_name is search("Server")
      register: systemstate
      failed_when: systemstate.rc not in [0, 4]
      changed_when: systemstate.rc == 0

    # --- Backup verification ---
    - name: Verify backup files exist
      ansible.windows.win_stat:
        path: "{{ backup_dir }}\\{{ item.name }}"
      loop: "{{ backup_sources }}"
      loop_control:
        label: "{{ item.name }}"
      register: verify
      failed_when: not verify.stat.exists

    # --- Cleanup old backups ---
    - name: Remove backups older than {{ retention_days }} days
      ansible.windows.win_shell: |
        $cutoff = (Get-Date).AddDays(-{{ retention_days }})
        $hostDir = "{{ backup_root }}\{{ inventory_hostname }}"
        Get-ChildItem $hostDir -Directory | Where-Object {
          try { [datetime]::ParseExact($_.Name, 'yyyy-MM-dd', $null) -lt $cutoff }
          catch { $false }
        } | Remove-Item -Recurse -Force
      register: cleanup
      changed_when: "'Remove-Item' in cleanup.stdout"

    # --- Summary ---
    - name: Backup summary
      ansible.builtin.debug:
        msg: |
          ✅ Backup completed for {{ inventory_hostname }}
          📁 Location: {{ backup_dir }}
          📦 Sources backed up: {{ backup_sources | length }}
          🗑️ Retention: {{ retention_days }} days

Backup with Scheduled Task

# schedule-backup.yml
---
- name: Schedule daily Windows backup
  hosts: windows_hosts

  tasks:
    - name: Copy backup script to host
      ansible.windows.win_copy:
        content: |
          # Daily backup script (managed by Ansible)
          $date = Get-Date -Format "yyyy-MM-dd"
          $dest = "\\backup-server\backups\$env:COMPUTERNAME\$date"
          New-Item -Path $dest -ItemType Directory -Force

          # Robocopy important folders
          robocopy "C:\ImportantData" "$dest\data" /MIR /Z /MT:8 /R:3 /W:10 /NFL /NDL /NP
          robocopy "C:\ProgramData\MyApp" "$dest\config" /MIR /Z /R:3 /NFL /NDL /NP

          # Log result
          "Backup completed: $(Get-Date)" | Out-File "$dest\backup.log"
        dest: "C:\\Scripts\\daily-backup.ps1"

    - name: Create scheduled task for daily backup
      community.windows.win_scheduled_task:
        name: "Ansible Daily Backup"
        description: "Daily backup managed by Ansible"
        actions:
          - path: "powershell.exe"
            arguments: "-ExecutionPolicy Bypass -File C:\\Scripts\\daily-backup.ps1"
        triggers:
          - type: daily
            start_boundary: "2026-01-01T02:00:00"  # 2 AM daily
        username: SYSTEM
        run_level: highest
        state: present
        enabled: true

    - name: Run backup immediately (first time)
      ansible.windows.win_shell: |
        Start-ScheduledTask -TaskName "Ansible Daily Backup"
      when: ansible_date_time.hour | int < 3  # Only if before 3 AM

Backup SQL Server Database

# backup-sqlserver.yml
---
- name: Backup SQL Server databases
  hosts: sql_servers
  vars:
    backup_dir: "D:\\SQLBackups\\{{ ansible_date_time.date }}"
    databases:
      - name: "ProductionDB"
        type: "FULL"
      - name: "ReportingDB"
        type: "FULL"

  tasks:
    - name: Create backup directory
      ansible.windows.win_file:
        path: "{{ backup_dir }}"
        state: directory

    - name: Backup each database
      ansible.windows.win_shell: |
        $db = "{{ item.name }}"
        $backupFile = "{{ backup_dir }}\${db}_{{ item.type }}_{{ ansible_date_time.date }}.bak"
        Invoke-Sqlcmd -Query "BACKUP DATABASE [$db] TO DISK='$backupFile' WITH {{ item.type }}, COMPRESSION, CHECKSUM"
        # Verify backup
        Invoke-Sqlcmd -Query "RESTORE VERIFYONLY FROM DISK='$backupFile'"
        Write-Output "Backup verified: $backupFile"
      loop: "{{ databases }}"
      loop_control:
        label: "{{ item.name }}"
      register: sql_backup

    - name: Copy backups to network share
      community.windows.win_robocopy:
        src: "{{ backup_dir }}"
        dest: "\\\\backup-server\\sql-backups\\{{ inventory_hostname }}\\{{ ansible_date_time.date }}"
        recurse: true
        flags:
          - /Z
          - /R:3

Pull Backups to Ansible Controller

# fetch-backup.yml — Pull files FROM Windows TO the controller
---
- name: Fetch backup files from Windows hosts
  hosts: windows_hosts

  tasks:
    - name: Create archive of config directory
      ansible.windows.win_shell: |
        Compress-Archive -Path "C:\ProgramData\MyApp\*" `
          -DestinationPath "C:\Temp\config-backup.zip" -Force

    - name: Fetch archive to controller
      ansible.builtin.fetch:
        src: "C:\\Temp\\config-backup.zip"
        dest: "/backups/{{ inventory_hostname }}/config-backup.zip"
        flat: true

    - name: Clean up temp archive
      ansible.windows.win_file:
        path: "C:\\Temp\\config-backup.zip"
        state: absent

Common Mistakes

# ❌ MISTAKE 1: Using win_copy for large directories (slow)
# Use win_robocopy instead — it's 10x faster for large folder trees

# ❌ MISTAKE 2: No error handling on network share access
# Always verify share is accessible before starting backup
- name: Check share access
  ansible.windows.win_shell: Test-Path "\\\\backup-server\\share"
  register: share_test
  failed_when: "'True' not in share_test.stdout"

# ❌ MISTAKE 3: Robocopy exit codes misunderstood
# Robocopy uses non-standard exit codes:
#   0 = No changes, 1 = Files copied, 2 = Extra files found
#   3 = Files copied + extra, 8+ = Errors
# community.windows.win_robocopy handles this correctly

# ❌ MISTAKE 4: No backup verification
# Always verify backups exist after creation:
- name: Verify backup
  ansible.windows.win_stat:
    path: "{{ backup_path }}"
  register: verify
  failed_when: not verify.stat.exists or verify.stat.size < 1024

# ❌ MISTAKE 5: No retention policy (disk fills up)
# Always include cleanup of old backups in your playbook

Inventory Example

# inventory/windows.ini
[windows_hosts]
web01.corp.local
web02.corp.local
dc01.corp.local

[windows_hosts:vars]
ansible_connection=winrm
ansible_winrm_transport=ntlm
ansible_winrm_server_cert_validation=ignore
ansible_port=5986

Conclusion

Ansible makes Windows backup automation repeatable and auditable. Use win_robocopy for large folder synchronization, win_copy for small config files, fetch to pull backups to the controller, and scheduled tasks for unattended daily backups. Combine with retention policies and verification steps for production-grade backup automation. Start with the complete backup playbook above and customize the source paths and retention period for your environment.