Windows Server 2016 sits on the 14393 build family, and that number never moves. A machine patched last week and one that has been untouched since 2017 report the exact same build — which is why "what version are we on?" is the wrong first question. Before a migration plan can be written you need three separate things: the vendor's official support dates, an inventory of hosts and roles, and the real hotfix level on each box.
The audit below takes me 60–90 minutes of hands-on work. Writing the actual migration plan afterwards takes longer, and how much longer depends on what the inventory turns up.
What you need before you start
- One Windows Server 2016 host to test the commands on, plus a management workstation with Windows PowerShell 5.1. Check
$PSVersionTable.PSVersion— if it reports 2.0, the cmdlets below will fail. - A domain account that is a local administrator on every target server and has read rights in Active Directory.
- The RSAT Active Directory PowerShell module on the workstation, for
Get-ADComputer. - WinRM reachable on the servers (TCP 5985 for HTTP, 5986 for HTTPS), or console/RDP as a fallback for hosts that don't answer.
- TCP 8530/8531 to your WSUS server if you also want to verify the patch path.
- A working folder. Disk space is a non-issue: a few hundred servers produce CSVs of a few hundred KB.
Three terms that come up constantly here: ESU (Extended Security Updates, a paid programme that keeps an out-of-support product patched), CIM (the standard Windows interface PowerShell uses to read OS and hardware data), and FSMO (the five special AD roles held by specific domain controllers).
New-Item -ItemType Directory -Path C:\Inventory -Force
Step 1: Pin the official dates and confirm the build
Open the Microsoft Lifecycle Policy page for Windows Server 2016 yourself and copy three items into C:\Inventory\lifecycle.md: the mainstream end date, the extended end date, and the exact wording of the Extended Security Updates section. I deliberately don't hard-code the dates in this post — they are published per product and per edition, and a blog post from last year is not a source.
Then read the OS identity from one representative server:
Get-CimInstance Win32_OperatingSystem |
Select-Object Caption, Version, BuildNumber, InstallDate, LastBootUpTime |
Format-List
Example output (values differ per host — the build family is the part that matters):
Caption : Microsoft Windows Server 2016 Standard
Version : 10.0.14393
BuildNumber : 14393
InstallDate : 11/14/2017 09:12:44
LastBootUpTime : 03/02/2025 06:01:10
10.0.14393 identifies Windows Server 2016, and the last four digits never change. That is precisely why the hotfix level has to be captured separately in Step 4.
Cross-check with winver on the same machine: the dialog should read "Windows Server 2016" and show the same build number. Your lifecycle.md should end up with three filled lines, each stamped with the date you checked the page, for example extended_end: <date from the page> (checked 2025-XX-XX). An empty line means the plan isn't grounded yet — go back to the page.
Step 2: Find every Windows Server 2016 host in AD
Query Active Directory for computer objects whose OS string mentions Windows Server 2016, then sort by last logon so the abandoned boxes surface first:
Get-ADComputer -Filter 'OperatingSystem -like "*Windows Server 2016*"' `
-Properties OperatingSystem, OperatingSystemVersion, LastLogonDate, DistinguishedName |
Select-Object Name, OperatingSystem, OperatingSystemVersion, LastLogonDate, DistinguishedName |
Sort-Object LastLogonDate |
Export-Csv C:\Inventory\ws2016-servers.csv -NoTypeInformation -Encoding UTF8
A row from that CSV should look like this:
"SRV-FS01","Windows Server 2016 Standard","10.0.14393","02/28/2025 21:40:11","CN=SRV-FS01,OU=FileServers,DC=contoso,DC=com"
Keep the DistinguishedName column — it tells you which OU the machine lives in, and that usually hints at who owns it.
Step 3: Capture installed roles and ownership
The migration path depends entirely on what a server actually does, so pull the role list over WinRM:
$servers = Import-Csv C:\Inventory\ws2016-servers.csv | Select-Object -ExpandProperty Name
Invoke-Command -ComputerName $servers -ErrorAction Continue -ScriptBlock {
[pscustomobject]@{
Server = $env:COMPUTERNAME
Roles = (Get-WindowsFeature | Where-Object Installed |
Select-Object -ExpandProperty Name) -join ';'
PSVersion = $PSVersionTable.PSVersion.ToString()
}
} | Select-Object Server, Roles, PSVersion |
Export-Csv C:\Inventory\ws2016-roles.csv -NoTypeInformation -Encoding UTF8
Get-WindowsFeature enumerates installed roles and features on a server OS. Capturing it once gives you the raw material for migration waves. Here is how I read the output:
Role found in Roles
|
Record before planning | Why a naive "rebuild the box" blocks you |
|---|---|---|
AD-Domain-Services |
Whether it holds FSMO roles (netdom query fsmo) |
You add a new DC, transfer the roles, then demote — never rebuild in place |
DNS, DHCP
|
Zone and scope exports, DHCP reservations | Clients lose name resolution or leases during cutover |
File-Services |
Share list, NTFS permissions, total size | The data copy window defines your maintenance window |
Web-Server |
Sites, bindings, certificates | TLS certificates must be exported or re-issued |
| Anything else | Vendor product name and support contact | Third-party software may not be certified on the new OS |
Then verify that every host answered:
(Import-Csv C:\Inventory\ws2016-servers.csv).Count
(Import-Csv C:\Inventory\ws2016-roles.csv).Count
Both numbers must match. A smaller second number means some hosts never responded over WinRM. Finally, fill in the two columns AD cannot give you — owner and business function — by hand, and confirm this checklist:
- [ ] Every host has a build number and a
LastLogonDate. - [ ] Every host has a role list, or is explicitly marked "unreachable — checked via console".
- [ ] Every host has an owner and a business function. Nobody in AD knows who cares about the machine.
- [ ] Inbound dependencies are recorded: who connects to it, on which port.
- [ ] Lifecycle dates copied from the vendor page, with the date you checked them.
Step 4: Verify the patch channel and dry-run one low-risk server
Next, confirm each server can still reach your patch infrastructure and is genuinely up to date:
Get-HotFix | Sort-Object InstalledOn -Descending |
Select-Object -First 5 HotFixID, Description, InstalledOn
Test-NetConnection <your-wsus-server> -Port 8530
Example output:
HotFixID Description InstalledOn
-------- ----------- -----------
KB50XXXXX Security Update 02/14/2025 00:00:00
KB50XXXXX Update 01/09/2025 00:00:00
ComputerName : <your-wsus-server>
RemotePort : 8530
TcpTestSucceeded : True
The InstalledOn column proves a server is actually patched, not merely supposed to be. TcpTestSucceeded : True proves the path to the patch server is open, so any later failure is configuration rather than the network.
With that done, pick one low-risk machine — a test box or a secondary file server — and stand up its replacement, running the same Get-CimInstance and role commands against the new host. If the output looks the way you expect, the procedure is repeatable and you can start scheduling waves. If it doesn't, you've found the problem on a machine nobody depends on.
Takeaway
The audit leaves you with three artefacts: lifecycle.md with the official dates and the day you checked them, ws2016-servers.csv with the host list, and ws2016-roles.csv plus hand-filled owner columns. Those three files are what a migration plan is actually built from, because roles decide the waves — domain controllers get add/transfer/demote, file servers live or die by the data copy window, web servers by their certificates. The build number alone tells you nothing about patch level, so capture the hotfix baseline separately and never trust a second-hand date for the support cutoff.
This article was originally published on AI Release — https://ai-release.org — where you'll find more hands-on infrastructure guides. New posts also land on the channel: https://t.me/ai_release1.
Top comments (0)