<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged awx</title>
<link>https://coderlegion.com/tag/awx</link>
<description>Powered by Question2Answer</description>
<item>
<title>Why win_updates Alone Isn't Enough for Production Windows Patching — My AWX Approach</title>
<link>https://coderlegion.com/30130/why-winupdates-alone-isnt-enough-for-production-windows-patching-my-awx-approach</link>
<description>&lt;p&gt;Most Windows patching guides show you a basic win_updates task and call it done. That works fine in a lab. But when you're managing 30 production servers, you need something that tells you what's going to happen before it happens, handles double reboots gracefully, auto-detects WSUS, and leaves a proper audit trail.&lt;/p&gt;
&lt;p&gt;This is the playbook I actually run at work through AWX — four phases, one server at a time, with a human approval checkpoint before anything gets touched.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pre-check&lt;/strong&gt; — OS build, pending reboots, disk space, WSUS detection, update scan&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Patch&lt;/strong&gt; — two-pass patching with automatic reboot handling&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Post-check&lt;/strong&gt; — OS build verification, uptime, installed hotfixes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validate&lt;/strong&gt; — build comparison before vs after, final ping test&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each phase runs as a separate AWX job template using Ansible tags — so you're in full control of what runs and when. We patch one server at a time (&lt;code&gt;serial: 1&lt;/code&gt;) so a failure on one host never cascades to the rest of the fleet.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-architecture-how-it-runs-in-awx&quot;&gt;The Architecture — How It Runs in AWX&lt;/h3&gt;
&lt;p&gt;Before looking at the playbook itself, here's how I've wired this up in AWX:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;AWX Workflow: Windows Patching
│
├── Job Template 1: Pre-Check (tags: precheck)
│ └── [On Success]
├── Job Template 2: Patch (tags: patch)
│ └── [On Success]
├── Job Template 3: Post-Check (tags: postcheck)
│ └── [On Success]
└── Job Template 4: Validate (tags: validate)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each job template runs the same playbook (&lt;code&gt;windows_patching_with_clear_logs.yaml&lt;/code&gt;) but with a different tag. This gives you full visibility in AWX — you can see exactly which phase passed or failed, and stop the workflow at any point before moving to the next phase.&lt;/p&gt;
&lt;p&gt;In our environment, all 30 servers are in the same AWX inventory. The playbook uses &lt;code&gt;serial: 1&lt;/code&gt; which means AWX runs through all four phases on server 1 before moving to server 2. One server at a time, fully controlled.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-playbook-variables&quot;&gt;The Playbook Variables&lt;/h3&gt;
&lt;p&gt;At the top of the playbook I define the key variables that control the patching behaviour:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;vars:
  patch_categories:
    - SecurityUpdates
    - CriticalUpdates
    - UpdateRollups
  windowslogs_dir: &quot;windowslogs/windows-patching&quot;
  patch_passes: 2
  upd_search_log: &quot;C:\\Windows\\Temp\\ans_upd_search.log&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;patch_categories&lt;/code&gt;&lt;/strong&gt; — the three update types I install. I deliberately exclude FeaturePacks and DefinitionUpdates to keep patching focused on security and stability.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;windowslogs_dir&lt;/code&gt;&lt;/strong&gt; — where log files get saved on the Ansible controller, not on the Windows servers. Every host gets its own timestamped log file so you have a full audit trail.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;patch_passes&lt;/code&gt;&lt;/strong&gt; — set to 2 because Windows often needs a second pass after a reboot to pick up remaining updates, especially Cumulative Updates and Servicing Stack updates.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;pre-tasks-setting-up-log-files&quot;&gt;Pre-Tasks — Setting Up Log Files&lt;/h3&gt;
&lt;p&gt;Before any tasks run, I set up timestamped log file paths on the controller:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;pre_tasks:
  - name: Build run timestamp on controller (for unique filenames)
    delegate_to: localhost
    run_once: true
    set_fact:
      date_str: &quot;{{ lookup('ansible.builtin.pipe', 'date +%Y%m%d_%H%M%S') }}&quot;
    tags: [always]

  - name: Ensure windowslogs dir exists on controller
    delegate_to: localhost
    run_once: true
    ansible.builtin.file:
      path: &quot;{{ windowslogs_dir }}&quot;
      state: directory
      mode: &quot;0755&quot;
    tags: [always]

  - name: Build per-host log file paths (on controller)
    set_fact:
      precheck_file: &quot;{{ windowslogs_dir }}/{{ inventory_hostname }}_precheck_{{ date_str }}.txt&quot;
      postcheck_file: &quot;{{ windowslogs_dir }}/{{ inventory_hostname }}_postcheck_{{ date_str }}.txt&quot;
      updates_file:  &quot;{{ windowslogs_dir }}/{{ inventory_hostname }}_updates_{{ date_str }}.txt&quot;
    tags: [always]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;tags: [always]&lt;/code&gt; means these tasks run regardless of which tag you pass — log paths are always set up first.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-1-pre-check&quot;&gt;Phase 1 — Pre-Check&lt;/h3&gt;
&lt;p&gt;This is the most important phase. I never patch a server without running pre-checks first.&lt;/p&gt;
&lt;h4 id=&quot;1-get-the-full-os-build-including-ubr&quot;&gt;1. Get the Full OS Build Including UBR&lt;/h4&gt;
&lt;p&gt;The standard OS build number isn't enough. Windows uses an &lt;strong&gt;Update Build Revision (UBR)&lt;/strong&gt; on top of the major build — so two servers with the same &lt;code&gt;OsBuildNumber&lt;/code&gt; might be at different patch levels:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Get OS version/build before patch (full build)
  ansible.windows.win_shell: |
    $ci = Get-ComputerInfo
    $cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
    $ubr = $cv.UBR
    $major = $ci.OsBuildNumber
    $full = '{0}.{1}' -f $major, $ubr
    [PSCustomObject]@{
      OsVersion     = $ci.OsVersion
      OsBuildNumber = $major
      UBR           = $ubr
      FullBuild     = $full
      Caption       = $ci.OsName
    } | ConvertTo-Json -Compress | Out-String
  register: pre_os
  check_mode: no
  changed_when: false
  tags: [precheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I then validate this output — if it comes back as UNKNOWN, the playbook fails immediately:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Validate pre_os output
  ansible.builtin.assert:
    that:
      - pre_os_version     != 'UNKNOWN'
      - pre_os_build_major != 'UNKNOWN'
      - pre_os_build_full  != 'UNKNOWN'
    fail_msg: &quot;Failed to parse OS info on {{ inventory_hostname }}.&quot;
  tags: [precheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-check-for-pending-reboots&quot;&gt;2. Check for Pending Reboots&lt;/h4&gt;
&lt;p&gt;If a server already has a pending reboot, patching it is risky. I check two registry keys:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Check pending reboot before patch (PowerShell fallback)
  ansible.windows.win_shell: |
    $paths = @(
      &quot;HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending&quot;,
      &quot;HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired&quot;
    )
    $pending = $false
    foreach ($p in $paths) { if (Test-Path $p) { $pending = $true } }
    if ($pending) { &quot;true&quot; } else { &quot;false&quot; }
  register: pre_reboot_raw
  check_mode: no
  changed_when: false
  tags: [precheck]

- name: Stop if a reboot is already pending
  ansible.builtin.fail:
    msg: &quot;Reboot is already pending on {{ inventory_hostname }}. Reboot first, then patch.&quot;
  when: pre_reboot.reboot_pending | default(false)
  tags: [precheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-check-disk-space&quot;&gt;3. Check Disk Space&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Check free space on system drive (bytes)
  ansible.windows.win_shell: |
    $sys = ([System.Environment]::GetEnvironmentVariable('SystemDrive'))
    Get-PSDrive -Name $sys.TrimEnd(':') |
    Select-Object Used,Free | ConvertTo-Json -Compress | Out-String
  register: pre_disk
  check_mode: no
  changed_when: false
  tags: [precheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;4-wsus-auto-detection&quot;&gt;4. WSUS Auto-Detection&lt;/h4&gt;
&lt;p&gt;One of the things I'm most proud of in this playbook. It auto-detects whether WSUS is configured and adjusts accordingly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Detect WSUS policy (is WSUS enforced?)
  ansible.windows.win_shell: |
    $wuKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'
    $auKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU'
    $obj = [ordered]@{
      UseWUServer = $null
      WUServer    = $null
      WUStatus    = $null
    }
    if (Test-Path $auKey) {
      $obj.UseWUServer = (Get-ItemProperty $auKey -ErrorAction SilentlyContinue).UseWUServer
    }
    if (Test-Path $wuKey) {
      $p = Get-ItemProperty $wuKey -ErrorAction SilentlyContinue
      $obj.WUServer  = $p.WUServer
      $obj.WUStatus  = $p.WUStatusServer
    }
    $obj | ConvertTo-Json -Compress | Out-String
  register: wsus_raw
  check_mode: no
  changed_when: false
  tags: [precheck]

- name: Decide update server selection (WSUS vs Microsoft Update)
  set_fact:
    update_server_selection: &amp;gt;-
      {{
        'managed_server'
        if (wsus_use_server == 1 and (wsus_url | length &amp;gt; 0))
        else 'windows_update'
      }}
  tags: [precheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;5-async-update-scan&quot;&gt;5. Async Update Scan&lt;/h4&gt;
&lt;p&gt;Before touching anything, I do a full scan without downloading or installing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Kick off update search (no download/install)
  ansible.windows.win_updates:
    state: searched
    category_names: &quot;{{ patch_categories }}&quot;
    server_selection: &quot;{{ update_server_selection }}&quot;
    log_path: &quot;{{ upd_search_log }}&quot;
  register: pre_search_async
  async: 7200
  poll: 0
  tags: [precheck]

- name: Wait up to 20 minutes for search to finish
  async_status:
    jid: &quot;{{ pre_search_async.ansible_job_id }}&quot;
  register: pre_search
  until: pre_search.finished
  retries: 40
  delay: 30
  tags: [precheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;6-predict-patching-complexity&quot;&gt;6. Predict Patching Complexity&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Predict complexity (long run / multi-reboot heuristic)
  set_fact:
    candidate_updates: &quot;{{ pre_search.updates | default([]) }}&quot;
    will_be_long: &amp;gt;-
      {{ (pre_search.updates | default([])
          | selectattr('title','search','Cumulative Update|Servicing Stack|Feature Update')
          | list | length) &amp;gt; 0 }}
    predicted_passes: &quot;{{ 2 if (pre_search.updates | default([])
          | selectattr('title','search','Cumulative Update|Servicing Stack')
          | list | length) &amp;gt; 0 else 1 }}&quot;
  tags: [precheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;7-save-pre-check-log&quot;&gt;7. Save Pre-Check Log&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Save pre-check summary (controller)
  delegate_to: localhost
  ansible.builtin.copy:
    dest: &quot;{{ precheck_file }}&quot;
    mode: &quot;0644&quot;
    content: |
      Host: {{ inventory_hostname }}
      Caption: {{ pre_os_caption }}
      OS Version: {{ pre_os_version }}
      OS Build (major): {{ pre_os_build_major }}
      OS Build (full) : {{ pre_os_build_full }}
      Last Boot: {{ pre_uptime.stdout | trim }}
      Pending Reboot: {{ pre_reboot.reboot_pending | default(false) }}
      Disk (bytes): {{ pre_disk.stdout | trim }}
      Candidate Updates: {{ (candidate_updates | length) if candidate_updates is defined else 0 }}
      Predicted Long/Multi-Reboot: {{ will_be_long | default(false) }}
      Suggested Passes: {{ predicted_passes | default(1) }}
      Updates:
      {% for u in candidate_updates %}
        - {{ u.title if (u is mapping and 'title' in u) else (u|string) }}
      {% endfor %}
  tags: [precheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-2-patch&quot;&gt;Phase 2 — Patch&lt;/h3&gt;
&lt;p&gt;Once pre-checks pass and I've reviewed the log, I launch the patch job template in AWX. Two passes — first installs and reboots, second catches anything remaining:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: PATCH PASS 1 - install updates
  ansible.windows.win_updates:
    category_names: &quot;{{ patch_categories }}&quot;
    server_selection: &quot;{{ update_server_selection }}&quot;
    reboot: yes
    reboot_timeout: 3600
  register: patch_pass1
  tags: [never, patch]

- name: Wait for host to return (pass 1)
  wait_for_connection:
    delay: 20
    timeout: 1200
  when: patch_pass1.reboot_required | default(false)
  tags: [never, patch]

- name: PATCH PASS 2 - install remaining updates
  ansible.windows.win_updates:
    category_names: &quot;{{ patch_categories }}&quot;
    server_selection: &quot;{{ update_server_selection }}&quot;
    reboot: yes
    reboot_timeout: 3600
  register: patch_pass2
  when: patch_passes|int &amp;gt;= 2
  tags: [never, patch]

- name: Wait for host to return (pass 2)
  wait_for_connection:
    delay: 20
    timeout: 1200
  when:
    - patch_passes|int &amp;gt;= 2
    - patch_pass2.reboot_required | default(false)
  tags: [never, patch]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;tags: [never, patch]&lt;/code&gt; — the &lt;code&gt;never&lt;/code&gt; means these tasks are skipped unless you explicitly pass &lt;code&gt;--tags patch&lt;/code&gt;. This prevents accidental patching.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-3-post-check&quot;&gt;Phase 3 — Post-Check&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Get OS version/build postchecks (full build)
  ansible.windows.win_shell: |
    $ci = Get-ComputerInfo
    $cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
    $ubr = $cv.UBR
    $major = $ci.OsBuildNumber
    $full = '{0}.{1}' -f $major, $ubr
    [PSCustomObject]@{
      OsVersion     = $ci.OsVersion
      OsBuildNumber = $major
      UBR           = $ubr
      FullBuild     = $full
    } | ConvertTo-Json -Compress | Out-String
  register: post_os
  tags: [never, postcheck]

- name: Get installed hotfixes postchecks
  ansible.windows.win_shell: &amp;gt;
    Get-HotFix | Sort-Object InstalledOn |
    Select-Object HotFixID, InstalledOn |
    Format-Table -HideTableHeaders | Out-String
  register: hotfixes
  tags: [never, postcheck]

- name: Save post-check summary (controller)
  delegate_to: localhost
  ansible.builtin.copy:
    dest: &quot;{{ windowslogs_dir }}/{{ inventory_hostname }}_postcheck_{{ date_str }}.txt&quot;
    mode: &quot;0644&quot;
    content: |
      Host: {{ inventory_hostname }}
      OS Version (post): {{ post_os_version }}
      OS Build (post, major): {{ post_os_build_major }}
      OS Build (post, full) : {{ post_os_build_full }}
      Last Boot (post): {{ post_uptime.stdout | trim }}
      Installed Updates:
      {{ hotfixes.stdout | trim }}
  tags: [never, postcheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-4-validate&quot;&gt;Phase 4 — Validate&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Summarize patch result
  ansible.builtin.debug:
    msg: |
      Patch Result for {{ inventory_hostname }}:
      - Categories: {{ patch_categories | join(', ') }}
      - Pass1: installed={{ (patch_pass1.updates | default([])) | length }},
               reboot={{ patch_pass1.reboot_required | default(false) }}
      - Pass2: installed={{ (patch_pass2.updates | default([])) | length
               if (patch_pass2 is defined) else 0 }},
               reboot={{ patch_pass2.reboot_required | default(false)
               if (patch_pass2 is defined) else false }}
      - OS Build Before (full): {{ pre_os_build_full | default('N/A') }}
      - OS Build After  (full): {{ post_os_build_full | default('N/A') }}
      - Status: {{
          'PATCH SUCCESSFUL - OS BUILD CHANGED'
          if (pre_os_build_full is defined and post_os_build_full is defined
              and pre_os_build_full != post_os_build_full)
          else 'NO BUILD CHANGE - verify installed hotfixes or CU cadence'
        }}
  tags: [never, validate]

- name: Final ping test
  ansible.windows.win_ping:
  tags: [never, validate]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;setting-this-up-in-awx&quot;&gt;Setting This Up in AWX&lt;/h3&gt;
&lt;h4 id=&quot;step-1-create-four-job-templates&quot;&gt;Step 1 — Create Four Job Templates&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Job Template &lt;/th&gt;&lt;th&gt;Playbook &lt;/th&gt;&lt;th&gt;Job Tags&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Windows Pre-Check &lt;/td&gt;&lt;td&gt;windows_patching_with_clear_logs.yaml &lt;/td&gt;&lt;td&gt;precheck&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Windows Patch &lt;/td&gt;&lt;td&gt;windows_patching_with_clear_logs.yaml &lt;/td&gt;&lt;td&gt;patch&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Windows Post-Check &lt;/td&gt;&lt;td&gt;windows_patching_with_clear_logs.yaml &lt;/td&gt;&lt;td&gt;postcheck&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Windows Validate &lt;/td&gt;&lt;td&gt;windows_patching_with_clear_logs.yaml &lt;/td&gt;&lt;td&gt;validate&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;step-2-create-the-workflow&quot;&gt;Step 2 — Create the Workflow&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;AWX UI → &lt;strong&gt;Templates&lt;/strong&gt; → &lt;strong&gt;Add&lt;/strong&gt; → &lt;strong&gt;Add Workflow Template&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;Windows Patching Workflow&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt; → &lt;strong&gt;Workflow Visualiser&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Chain the four job templates together using &lt;strong&gt;On Success&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pre-Check → [On Success] → Patch → [On Success] → Post-Check → [On Success] → Validate&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;step-3-add-a-manual-approval-node&quot;&gt;Step 3 — Add a Manual Approval Node&lt;/h4&gt;
&lt;p&gt;Between Pre-Check and Patch I add a manual &lt;strong&gt;Approval&lt;/strong&gt; node so someone reviews the pre-check logs before patching starts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In the Workflow Visualiser → between Pre-Check and Patch&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;+&lt;/strong&gt; → select &lt;strong&gt;Approval&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;Review Pre-Check Logs Before Patching&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Timeout: 4 hours&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the human checkpoint before anything touches production.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;errors-i-hit-in-production&quot;&gt;Errors I Hit in Production&lt;/h3&gt;
&lt;h4 id=&quot;error-1-winrm-drops-after-reboot-and-never-comes-back&quot;&gt;Error 1 — WinRM Drops After Reboot and Never Comes Back&lt;/h4&gt;
&lt;p&gt;Fix — increase the &lt;code&gt;wait_for_connection&lt;/code&gt; timeout:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;wait_for_connection:
  delay: 20
  timeout: 1800
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;error-2-wsus-returning-no-updates&quot;&gt;Error 2 — WSUS Returning No Updates&lt;/h4&gt;
&lt;p&gt;WSUS hadn't approved the latest Cumulative Update. Fix — override at runtime in AWX by passing as an extra variable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;update_server_selection: windows_update
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;error-3-ubr-not-changing-after-patching&quot;&gt;Error 3 — UBR Not Changing After Patching&lt;/h4&gt;
&lt;p&gt;Check &lt;code&gt;Get-HotFix&lt;/code&gt; output in the post-check log. If new KBs are listed even though the UBR didn't change, only smaller updates installed — worth investigating before signing off.&lt;/p&gt;
&lt;h4 id=&quot;error-4-async-update-scan-timing-out&quot;&gt;Error 4 — Async Update Scan Timing Out&lt;/h4&gt;
&lt;p&gt;Fix — increase async timeout and retries:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;async: 7200
retries: 80
delay: 30
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-this-gives-you&quot;&gt;What This Gives You&lt;/h3&gt;
&lt;p&gt;For 30 production Windows servers, this approach gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;full audit trail&lt;/strong&gt; — pre and post check logs per server per patching run&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safety gates&lt;/strong&gt; — pending reboot detection, disk space checks and manual approval before patching starts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Intelligence&lt;/strong&gt; — WSUS auto-detection means the same playbook works across different network segments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visibility in AWX&lt;/strong&gt; — each phase is a separate node in the workflow so you can see exactly where something failed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Confidence&lt;/strong&gt; — build comparison before and after tells you definitively whether the Cumulative Update actually applied&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-s-next&quot;&gt;What's Next&lt;/h3&gt;
&lt;p&gt;The next improvement I'm planning is adding email notifications to this workflow — so when patching fails on any of the 30 servers at 2AM, I get an alert immediately rather than finding out in the morning.&lt;/p&gt;
&lt;p&gt;Drop your questions in the comments — happy to help!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;— Sireesha&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30130/why-winupdates-alone-isnt-enough-for-production-windows-patching-my-awx-approach</guid>
<pubDate>Wed, 07 Oct 2026 11:41:31 +0000</pubDate>
</item>
</channel>
</rss>