Moving a Nine-Year-Old Windows Server from Xen to Nitro: t2.large → t3.large Without a Rebuild
By Sergey Nosov
July 15, 2026
Last night I migrated a production EC2 instance from t2.large to t3.large — which sounds
like a five-minute stop/change-type/start until you learn that the box was a Windows Server 2016 install that had
been running since the Xen era, and that a t3 (Nitro) instance won't even boot without storage and
network drivers the old machine never had. This is the write-up I wish I'd found beforehand: what worked, what
broke, and the IAM permission whack-a-mole AWS's own documentation walked me into.
Why bother
Three reasons, in honesty-order:
- Money. The instance had been covered by back-to-back 3-year Reserved Instances since 2017. The
last one expired, and the box silently reverted to on-demand — about $88/month for a Windows
t2.largein us-west-2. Just resizing tot3.largecuts that ~31% with no commitment; reserving the t3 for three years brings it to ~$43/month, cheaper than renewing the t2 ever would have been. - Hardware. Same 2 vCPU / 8 GB, but Nitro instead of Xen: better network (the in-guest NIC reports 5 Gbps burst, up from 1), better EBS throughput, and a modern credit model.
- A deadline with teeth. The 2016 OS needs an in-place upgrade before its extended support ends. Doing the boot-critical driver work first, on a known-good OS, isolates one risky change from the other.
The numbers that drove it (us-west-2, Windows, License Included):
t2.largeon-demand — where the expired RI left us: $88/montht3.largeon-demand — just resize: $61/montht3.large3-year all-upfront RI: $43/month
The blocker: a t3 is not a bigger t2
On Nitro instances, EBS volumes appear as NVMe devices and networking is ENA. My driver inventory (run this before you plan anything):
Get-WindowsDriver -Online | ? { $_.ProviderName -like '*Amazon*' } |
Select ClassName, ProviderName, Version, Date, OriginalFileName
The results: AWS NVMe driver absent (a t3 boot would die with
INACCESSIBLE_BOOT_DEVICE), ENA at 1.0.8 from 2016 (needs ≥ 2.2.3), AWS PV at 7.4.6 from 2017. A
nine-year-old Windows image simply predates the hardware it's about to run on.
Tip: enaSupport=true on the instance attribute tells you nothing about the
OS. Mine had the attribute set for years; the driver that makes it mean something was never installed.
Preparation that paid off
Pre-warm your snapshots the day before. The first-ever snapshot of an EBS volume copies every block
and can take hours. I took crash-consistent snapshots of both volumes while the instance was running, the day
before the window. Result: the real backup — a stopped-state AMI taken inside the window — completed in 14
minutes instead of potentially hours of downtime. (Related: aws ec2 wait image-available gives up
after ~10 minutes and reports failure. If the AMI still shows pending, nothing is wrong — just run the
wait again.) The pre-warm snapshots are disposable the moment the migration is confirmed good.
Capture a network baseline. When Windows switches from the Xen PV adapter to ENA, it's a brand-new
NIC as far as the OS is concerned — any static IP or DNS configured inside Windows silently resets to
DHCP. Save ipconfig /all and Get-NetAdapter | Format-List * to a file you keep
off the box. Mine showed everything on DHCP, so I knew in advance the switch would be a
non-event.
Interrogate Windows Update properly. My first check looked clean — no reboot pending. The full query told a different story:
Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
(New-Object -ComObject Microsoft.Update.Session).CreateUpdateSearcher().Search(
"IsInstalled=0 and IsHidden=0").Updates | Select Title
Five updates queued, including the monthly cumulative and a servicing-stack update. A maintenance window that accidentally turns into "Working on updates, 3 of 47" on a Server 2016 box is a window you no longer control. Install them, reboot, and re-check before the window — the SSU can unlock a second round.
Inventory the blast radius — the box always does more than you think. Mine was "the web server"
right up until Get-Service said it was also the mail server (hMailServer, primary MX for three
domains, plus POP3/IMAP), four database engines (SQL Server, MySQL, PostgreSQL, and a Sybase SQL Anywhere 8 that
predates some of my readers), and — surprise — authoritative DNS for two of the domains. Stopping
"the web server" meant stopping a nameserver. What saved the window: the secondary NS and backup MX lived on a
different host. Verify that before you take the primary down — query the secondary directly and compare
SOA serials:
Resolve-DnsName example.com -Type SOA -Server <secondary-ip>
(And a trap inside that check: when I probed the backup MX on port 25 from home, the connection failed — and so did a control test against the primary MX that demonstrably receives mail all day. Most residential ISPs block outbound port 25. A failed SMTP connection test from your desk proves nothing; control-test against a known-working target before drawing conclusions.)
Quiesce services explicitly; don't trust shutdown to do it. During a Windows shutdown each service
gets only a few seconds (WaitToKillServiceTimeout) before being killed — a database mid-checkpoint can
need more. Stop-Service waits for a clean stop and shows you the result. Stop writers before their
databases (the mail server writes into a DB — stop it first). And a small one: iisreset /stop
does not stop the FTP service; Stop-Service ftpsvc is on you.
Know your stop semantics. aws ec2 stop-instances without --force performs
the same orderly Windows shutdown as the Start menu — only --force is a power pull. Also check
InstanceInitiatedShutdownBehavior is stop (not terminate!) and consider
belt-and-braces guardrails: API termination protection plus an explicit IAM Deny on
ec2:TerminateInstances for the operator credentials you'll be using at 10pm.
Driver prep: the automation, and why it took four attempts
AWS's documented path is either manual driver installs over RDP (PV, ENA, NVMe, then a PnP-only sysprep) or the
AWSSupport-UpgradeWindowsAWSDrivers SSM automation. My box wasn't SSM-managed (the
agent ran, but with no instance profile it never registered), which forces offline mode: the
automation stops the instance, takes a backup AMI, builds a helper instance via CloudFormation, physically moves
your root volume to the helper, injects the drivers into the offline Windows image, and moves it back. Same volume
ID throughout — your rollback documentation stays true.
Offline mode has a hidden bonus: the drivers are injected with their boot-critical registry wiring, so the manual
path's Sysprep_Generalize_Pnp step isn't needed at all.
Here's the part that cost me three failed attempts: the documented IAM permission list for this automation is stale. I built a least-privilege operator policy from AWS's own docs pages, and it had four gaps — three found the hard way, one caught in between:
- Attempt 1 died in seconds on
autoscaling:DescribeAutoScalingInstances— the workflow's very first pre-check asserts your instance isn't in an Auto Scaling group. Not in the docs. - Attempt 2 died inside CloudFormation on
ec2:CreateLaunchTemplate— the modern template builds the helper via a launch template, and the documented permission list predates that change. - Between attempts, a sweep of the live workflow document surfaced
cloudformation:DescribeStackResource— singular, a different IAM action from theDescribeStackResources(plural) I had granted. It would have killed the run right after stack creation. - Attempt 3 died on the sneakiest one:
iam:TagRole/iam:TagInstanceProfile. The template defines no tags at all — I had verified that. But CloudFormation stamps invisibleaws:cloudformation:stack-name/stack-id/logical-idsystem tags on every taggable resource it creates, and that requires tag permissions. The error (UnauthorizedTaggingOperation) doesn't care that your template audit was clean.
The lesson, and the tip that actually ends the whack-a-mole: stop trusting documentation and interrogate the artifacts themselves. All of it is readable:
# the live automation document - grep the step list for every "Api": call it makes
aws ssm get-document --name AWSSupport-UpgradeWindowsAWSDrivers --query Content --output text
# the exact CloudFormation template a failed stack used - retrievable up to 90 days AFTER deletion
aws cloudformation get-template --stack-name <full-stack-ARN-with-id>
Then prove the policy before burning another maintenance window, with the IAM policy simulator:
aws iam simulate-principal-policy --policy-source-arn arn:aws:iam::<acct>:user/<operator> `
--action-names iam:CreateRole iam:TagRole ec2:CreateLaunchTemplate cloudformation:CreateStack ... `
--resource-arns <the-real-ARN-patterns>
One subtlety: if your policy uses Condition blocks (mine region-locked everything on
aws:RequestedRegion and constrained iam:PassRole by service), pass matching
--context-entries — otherwise the simulator reports false denials for statements whose conditions were
never given a value.
And validate the gate itself: I ran the simulation against the still-broken policy first. It flagged the permission
that had just failed for real (iam:TagRole) and its sibling that would have failed next
(iam:TagInstanceProfile) — and nothing else. A verifier that reproduces the known failure and predicts
the next one is a verifier you can trust. After the fixed policy simulated clean, attempt four ran end-to-end
without a hitch.
One reassurance earned from watching it fail three times: every failure happened in the automation's setup phase — pre-checks and stack creation — before my root volume was ever touched, and each time it cleanly restarted the instance itself. The blast radius of a permission failure is wasted window time, not a broken server.
Small gotchas that would each have cost an hour
The automation ends by starting your instance. Whatever state it found the box in, the parent document's final step boots it — on Xen, still a t2. I'd planned to resize while it was conveniently stopped; the automation won that race. No harm: it boots fine on Xen with the new drivers (they coexist), you verify the driver store over RDP, and then do your own stop → resize → start. Budget for that extra cycle.
A resized instance keeps its CPU credit mode. "T3 defaults to unlimited" applies only to
newly launched instances — the credit specification is per-instance state that rides through a resize.
Mine was standard (the t2 default); left alone, the shiny new t3 would have throttled at its
30%-per-vCPU baseline exactly like the t2 used to, quietly undercutting half the point of migrating. And the fix is
its own IAM action (ec2:ModifyInstanceCreditSpecification — not covered by
ModifyInstanceAttribute):
aws ec2 modify-instance-credit-specification `
--instance-credit-specifications "InstanceId=i-...,CpuCredits=unlimited"
If you go unlimited, watch CPUSurplusCreditsCharged in CloudWatch for the first weeks —
sustained surplus charges mean you need a bigger instance, not a credit tweak.
EC2Launch's installer extracts the zip itself. The automation upgrades drivers only — updating
EC2Launch (password generation, console output on Nitro) is still manual, and install.ps1 expects a
file named exactly EC2-Windows-Launch.zip sitting next to it. If you helpfully
Expand-Archive first, it exits with "zip not found". Download both files into one directory and just
run the script.
Server 2016 lets you split the windows. On 2008 R2/2012 you must resize immediately after driver prep; on 2016+ the box keeps booting happily on Xen afterwards, so driver prep and the resize can be separate maintenance windows if scheduling demands it.
Backup artifacts multiply — tag them at creation. The automation takes its own backup AMI
per attempt ("it is your responsibility to… delete"), on top of your step-0 rollback AMI and the
pre-warm snapshots. Four attempts in, I owned five AMIs. Pass --tag-specifications on everything you
create so cleanup day is a tag filter, not an archaeology dig — and the tags double as a state-recovery mechanism
if you ever have to reconstruct mid-migration progress from the AWS account alone.
The final sequence and the score
The window that actually worked: quiesce services → API stop → stopped-state rollback AMI (14 min, thanks
to pre-warming) → offline driver automation (~15 min) → RDP driver-store verification (NVMe present and
running, ENA 2.11) → stop → modify-instance-attribute to t3.large →
start.
First Nitro boot: 2/2 status checks, no drama. All sites 200, DNS answering, mail flowing, ten
services up, 5 Gbps NIC. (Had that boot gone dark instead, the diagnostic from outside is
aws ec2 get-console-screenshot — it distinguishes "Windows is grinding through PnP device setup, be
patient" from an INACCESSIBLE_BOOT_DEVICE stop error. And the rollback for a bad Nitro boot is
trivially cheap: stop, flip the type back to t2, start — same disks, back on Xen.)
Total downtime across the evening, including three failed automation attempts: about 52 minutes. Cost, effective immediately: −31%; after the 3-year reservation, −51%.
The checklist I'd hand my past self
- Driver inventory first — NVMe presence decides everything
- Pre-warm snapshots the day before; take the real backup AMI stopped
- Baseline
ipconfig /alloff-box; expect the ENA adapter to be a "new" NIC - Full Windows Update queue check, not just the reboot flag; clear it pre-window
Get-Serviceinventory — find the mail server, databases, and DNS roles you forgot; verify secondaries are off-box- Explicit
Stop-Servicequiesce (writers before databases;ftpsvcby hand) - Termination protection + IAM deny + shutdown-behavior
stop - For the SSM automation: pull the live document and CFN template, derive permissions from them, and
prove the policy with
simulate-principal-policybefore the window - Expect the automation to boot you back onto Xen; verify drivers, then resize yourself
- Fix the credit mode after resizing — it does not fix itself
- Update EC2Launch (zip next to
install.ps1, unextracted) and set the Nitro power settings - Tag every backup artifact at creation; calendar the cleanup — automation AMIs accumulate one per attempt
- Buy the reservation only after the instance is confirmed running on its final size
Nine years on Xen, one evening to leave. The hardware migration was the easy part — the real work was refusing to trust anything, including the documentation, without checking it against the machine.