Introduction

Enterprise Windows environments are complex: Active Directory forests with dozens of OUs, Group Policy Objects layered across sites, IIS farms serving internal applications, SQL Server clusters, and thousands of endpoints needing patching. Ansible manages all of it without installing agents — using WinRM or SSH to connect, PowerShell under the hood, and the same playbook/role/inventory patterns as Linux automation.

Setup

WinRM Configuration on Windows Targets

# Run on Windows targets (as Administrator)
# Option 1: Quick setup script from Ansible
$url = "https://raw.githubusercontent.com/ansible/ansible-documentation/main/examples/scripts/ConfigureRemotingForAnsible.ps1"
$file = "$env:temp\ConfigureRemotingForAnsible.ps1"
(New-Object -TypeName System.Net.WebClient).DownloadFile($url, $file)
powershell.exe -ExecutionPolicy ByPass -File $file

# Option 2: Manual HTTPS setup
winrm quickconfig -transport:https
winrm set winrm/config/service '@{AllowUnencrypted="false"}'
winrm set winrm/config/service/auth '@{Basic="true"}'
winrm set winrm/config/service/auth '@{CredSSP="true"}'

Ansible Control Node Configuration

pip install pywinrm requests-credssp
# group_vars/windows.yml
ansible_connection: winrm
ansible_winrm_transport: credssp
ansible_winrm_server_cert_validation: validate
ansible_port: 5986
ansible_user: "{{ vault_windows_admin }}"
ansible_password: "{{ vault_windows_password }}"

Collections

ansible-galaxy collection install ansible.windows
ansible-galaxy collection install community.windows
ansible-galaxy collection install microsoft.ad

Pattern 1: Active Directory Management

Create OUs and Users

---
- name: Manage Active Directory
  hosts: domain_controllers
  vars:
    ad_domain: example.com
    base_dn: "DC=example,DC=com"

  tasks:
    - name: Create organizational units
      microsoft.ad.ou:
        name: "{{ item.name }}"
        path: "{{ item.path }}"
        state: present
        protected: true
      loop:
        - { name: "IT Department", path: "{{ base_dn }}" }
        - { name: "Users", path: "OU=IT Department,{{ base_dn }}" }
        - { name: "Groups", path: "OU=IT Department,{{ base_dn }}" }
        - { name: "Service Accounts", path: "OU=IT Department,{{ base_dn }}" }
        - { name: "Servers", path: "OU=IT Department,{{ base_dn }}" }

    - name: Create AD security groups
      microsoft.ad.group:
        name: "{{ item.name }}"
        path: "OU=Groups,OU=IT Department,{{ base_dn }}"
        scope: global
        category: security
        description: "{{ item.description }}"
        state: present
      loop:
        - { name: "IT-Admins", description: "IT administrators" }
        - { name: "IT-Developers", description: "Development team" }
        - { name: "IT-Operations", description: "Operations team" }
        - { name: "SVC-Ansible", description: "Ansible service accounts" }

    - name: Create user accounts
      microsoft.ad.user:
        name: "{{ item.name }}"
        firstname: "{{ item.first }}"
        surname: "{{ item.last }}"
        password: "{{ item.password }}"
        state: present
        path: "OU=Users,OU=IT Department,{{ base_dn }}"
        email: "{{ item.email }}"
        upn: "{{ item.email }}"
        enabled: true
        password_never_expires: false
        user_cannot_change_password: false
        groups:
          add: "{{ item.groups }}"
        update_password: on_create
      loop: "{{ ad_users }}"
      no_log: true

    - name: Create Ansible service account
      microsoft.ad.user:
        name: svc-ansible
        firstname: Ansible
        surname: Service
        password: "{{ vault_ansible_svc_password }}"
        state: present
        path: "OU=Service Accounts,OU=IT Department,{{ base_dn }}"
        password_never_expires: true
        user_cannot_change_password: true
        groups:
          add:
            - SVC-Ansible
            - Domain Admins
      no_log: true

Bulk User Import from CSV

- name: Import users from CSV
  hosts: domain_controllers
  tasks:
    - name: Read CSV file
      ansible.builtin.read_csv:
        path: files/new_users.csv
      register: user_csv
      delegate_to: localhost

    - name: Create users from CSV
      microsoft.ad.user:
        name: "{{ item.samAccountName }}"
        firstname: "{{ item.FirstName }}"
        surname: "{{ item.LastName }}"
        password: "{{ item.Password }}"
        path: "{{ item.OU }}"
        email: "{{ item.Email }}"
        enabled: true
        update_password: on_create
        state: present
      loop: "{{ user_csv.list }}"
      no_log: true

Pattern 2: Windows Update Management

---
- name: Patch Windows servers — rolling update
  hosts: windows_servers
  serial: "25%"
  max_fail_percentage: 10

  tasks:
    - name: Create pre-patch snapshot (VMware)
      community.vmware.vmware_guest_snapshot:
        hostname: "{{ vcenter_hostname }}"
        username: "{{ vcenter_username }}"
        password: "{{ vcenter_password }}"
        name: "{{ inventory_hostname }}"
        snapshot_name: "pre-patch-{{ ansible_date_time.date }}"
        state: present
      delegate_to: localhost

    - name: Install critical and security updates
      ansible.windows.win_updates:
        category_names:
          - CriticalUpdates
          - SecurityUpdates
          - UpdateRollups
        reboot: true
        reboot_timeout: 3600
        state: installed
      register: update_result

    - name: Display update results
      ansible.builtin.debug:
        msg: |
          Updates installed: {{ update_result.installed_update_count }}
          Reboot required: {{ update_result.reboot_required }}
          Updates: {{ update_result.filtered_updates | map(attribute='title') | list }}

    - name: Verify services after patching
      ansible.windows.win_service_info:
        name: "{{ item }}"
      register: service_check
      loop:
        - W3SVC
        - MSSQLSERVER
        - WinRM
      failed_when: service_check.services[0].state != "running"

    - name: Run post-patch health check
      ansible.windows.win_shell: |
        $services = @('W3SVC', 'MSSQLSERVER', 'WinRM')
        $failed = @()
        foreach ($svc in $services) {
          $s = Get-Service -Name $svc -ErrorAction SilentlyContinue
          if ($s -and $s.Status -ne 'Running') { $failed += $svc }
        }
        if ($failed.Count -gt 0) { throw "Failed services: $($failed -join ', ')" }
        Write-Output "All services healthy"
      register: health_check

Pattern 3: IIS Web Server Management

---
- name: Configure IIS web farm
  hosts: iis_servers
  tasks:
    - name: Install IIS with features
      ansible.windows.win_feature:
        name:
          - Web-Server
          - Web-Asp-Net45
          - Web-ISAPI-Ext
          - Web-ISAPI-Filter
          - Web-Mgmt-Console
          - Web-Scripting-Tools
          - Web-Filtering
          - Web-Stat-Compression
          - Web-Dyn-Compression
          - Web-Url-Auth
        state: present
        include_management_tools: true
      register: iis_install

    - name: Reboot if features required restart
      ansible.windows.win_reboot:
      when: iis_install.reboot_required

    - name: Create application pool
      community.windows.win_iis_webapppool:
        name: MyApp-Pool
        state: started
        attributes:
          managedRuntimeVersion: v4.0
          managedPipelineMode: Integrated
          startMode: AlwaysRunning
          processModel.idleTimeout: "00:00:00"
          processModel.identityType: SpecificUser
          processModel.userName: "EXAMPLE\\svc-myapp"
          processModel.password: "{{ vault_svc_myapp_password }}"
          recycling.periodicRestart.time: "00:00:00"

    - name: Create IIS website
      community.windows.win_iis_website:
        name: MyApp
        state: started
        port: 443
        ssl: true
        physical_path: C:\inetpub\myapp
        application_pool: MyApp-Pool
        parameters: |
          bindings.[{protocol:'https',bindingInformation:'*:443:app.example.com'}]

    - name: Deploy application files
      ansible.windows.win_copy:
        src: files/myapp/
        dest: C:\inetpub\myapp\
      notify: Recycle app pool

    - name: Deploy web.config
      ansible.windows.win_template:
        src: templates/web.config.j2
        dest: C:\inetpub\myapp\web.config
      notify: Recycle app pool

  handlers:
    - name: Recycle app pool
      community.windows.win_iis_webapppool:
        name: MyApp-Pool
        state: restarted

Pattern 4: Registry and Group Policy

---
- name: Configure Windows security baseline via registry
  hosts: windows_servers
  tasks:
    - name: Disable SMBv1
      ansible.windows.win_regedit:
        path: HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters
        name: SMB1
        data: 0
        type: dword

    - name: Enable Windows Firewall for all profiles
      ansible.windows.win_shell: |
        Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled True

    - name: Configure audit policies
      ansible.windows.win_shell: |
        auditpol /set /subcategory:"Logon" /success:enable /failure:enable
        auditpol /set /subcategory:"Account Lockout" /success:enable /failure:enable
        auditpol /set /subcategory:"Special Logon" /success:enable /failure:enable
        auditpol /set /subcategory:"Security Group Management" /success:enable
        auditpol /set /subcategory:"User Account Management" /success:enable /failure:enable

    - name: Set password policy
      ansible.windows.win_shell: |
        net accounts /minpwlen:15 /maxpwage:365 /minpwage:1 /uniquepw:24 /lockoutthreshold:5

    - name: Configure Windows Update settings
      ansible.windows.win_regedit:
        path: HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU
        name: "{{ item.name }}"
        data: "{{ item.value }}"
        type: dword
      loop:
        - { name: "NoAutoUpdate", value: 0 }
        - { name: "AUOptions", value: 3 }
        - { name: "ScheduledInstallDay", value: 0 }
        - { name: "ScheduledInstallTime", value: 3 }

    - name: Disable unnecessary services
      ansible.windows.win_service:
        name: "{{ item }}"
        start_mode: disabled
        state: stopped
      loop:
        - RemoteRegistry
        - Spooler
        - TapiSrv
        - SNMPTRAP
      failed_when: false

Pattern 5: Windows Fleet Inventory

---
- name: Collect Windows fleet inventory
  hosts: windows_servers
  tasks:
    - name: Gather Windows facts
      ansible.builtin.setup:

    - name: Get installed software
      ansible.windows.win_shell: |
        Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* |
          Select-Object DisplayName, DisplayVersion, Publisher |
          ConvertTo-Json
      register: installed_software

    - name: Get disk information
      ansible.windows.win_shell: |
        Get-WmiObject Win32_LogicalDisk -Filter "DriveType=3" |
          Select-Object DeviceID, @{N='SizeGB';E={[math]::Round($_.Size/1GB,2)}},
          @{N='FreeGB';E={[math]::Round($_.FreeSpace/1GB,2)}},
          @{N='UsedPct';E={[math]::Round(($_.Size-$_.FreeSpace)/$_.Size*100,1)}} |
          ConvertTo-Json
      register: disk_info

    - name: Get pending updates
      ansible.windows.win_updates:
        state: searched
        category_names:
          - CriticalUpdates
          - SecurityUpdates
      register: pending_updates

    - name: Build inventory report
      ansible.builtin.template:
        src: windows-inventory-report.html.j2
        dest: "reports/{{ inventory_hostname }}_inventory.html"
      delegate_to: localhost
      vars:
        os_version: "{{ ansible_distribution }} {{ ansible_distribution_version }}"
        software_list: "{{ installed_software.stdout | from_json }}"
        disks: "{{ disk_info.stdout | from_json }}"
        updates_pending: "{{ pending_updates.found_update_count }}"

Troubleshooting

WinRM Connection Failed

# Test WinRM from control node
python3 -c "
import winrm
s = winrm.Session('https://server:5986', auth=('user', 'pass'), transport='credssp', server_cert_validation='validate')
print(s.run_cmd('hostname'))
"

# Check WinRM listeners on Windows
winrm enumerate winrm/config/listener

# Verify HTTPS listener exists
winrm get winrm/config/service

CredSSP Authentication Errors

# On Windows target
Enable-WSManCredSSP -Role Server -Force

# On control node (if Windows)
Enable-WSManCredSSP -Role Client -DelegateComputer *.example.com -Force

PowerShell Execution Policy

# Set in ansible.cfg or group_vars
ansible_winrm_operation_timeout_sec: 120
ansible_winrm_read_timeout_sec: 150

Conclusion

Enterprise Windows automation with Ansible covers the full stack: Active Directory management with microsoft.ad, Windows Updates with rolling patch windows, IIS deployment with app pool configuration, registry-based security hardening, and fleet-wide inventory collection. Use CredSSP for domain-joined authentication, serial for rolling updates that don't take down the whole farm, and AAP surveys for self-service AD user creation. The same Ansible patterns that work for Linux — roles, inventory groups, handlers, Vault — work identically for Windows at enterprise scale.