[SQL 4.1] Build Sheet: Migrating to a SQL Server Azure VM

Build the target properly, move a 2012-era estate onto it with the Arc portal migration experience or with backup to URL, register the IaaS extension, set the license type deliberately, and land the database at compatibility level 110 with a baseline before anything is raised.


By the end of this build sheet you have a SQL Server 2022 virtual machine built to the storage guidance rather than to the portal defaults, registered with the IaaS Agent extension, carrying a deliberately chosen license type, patched by Azure Update Manager rather than the feature that retires in 2027, and holding a database migrated from a SQL Server 2012 instance at its original compatibility level with a Query Store baseline captured before anything is raised.

Prerequisites: a virtual network with connectivity to the source estate and a subnet for the database tier; Contributor on the target resource group; the source SQL Server reachable and at least at SQL Server 2012 Service Pack 1 Cumulative Update 2 if you intend to back up to a URL; a storage account for backup staging in the same region as the target if you use the portal migration experience; the source server Arc-enabled if you want that experience at all, which also gets you the free assessment; a decision on license type made with whoever owns the licensing agreement, not assumed; and an agreed outage window, because unless you are building a distributed availability group this is an offline move.

Documented, and observed

The portal migration experience described in step 4 reached general availability in June 2026 and was publicly announced a few weeks before this was written. It is the newest surface in this series and the one with the least community coverage, so a word on confidence is owed. Its existence, its entry point, its underlying mechanism and its constraints are documented and cited. The precise blade labels are the part most likely to have shifted by the time you read this, and I have deliberately named the durable nouns rather than a click path. Be aware also that at least one of its own preparation pages still carried a preview banner after the feature went generally available, so if you find a page that contradicts the release notes, the release notes are the newer source. Step 5’s manual path is long-established and is the one to use if any of this looks unfamiliar in your portal.

Naming convention

vm-sql-<workload>-<env>-<nn> for the machine, with data, log and tempdb volumes as sqldata, sqllog and sqltemp so that any script written against one machine works on all of them. Backup blobs as <source-instance>/<database>-<yyyyMMdd-HHmm>.bak. The worked example builds vm-sql-fin-prod-01 from a source called SQL2012-FIN01.

Step 1: Choose the image and the license type together

These are one decision, not two, because the marketplace image you pick determines how the software is billed and switching later is a property change rather than a rebuild only if you set it up correctly now.

SettingValueWhy
Marketplace imageSQL Server 2022 on Windows Server 2022, or SQL Server 2025 on Windows Server 2025Images are published for 2025, 2022, 2019, 2017 and 2016. SQL Server 2012 and 2014 images have been withdrawn, so you cannot land a like-for-like 2012 target even if you wanted one. Choose the newest version your vendors will certify; the compatibility level protects you from the optimiser, not the vendor’s support policy.
EditionStandard unless a specific feature requires EnterpriseEnterprise licensing costs roughly four times the virtual machine it runs on. Make somebody name the feature.
License typeAHUB if you hold licenses with Software Assurance, PAYG if not, DR for a passive disaster recovery replicaThe IaaS Agent extension changes this with no downtime, so it is correctable, but the default is pay-as-you-go and nobody comes back to fix it. A passive replica set to the disaster recovery license type is free where entitlement exists.
Virtual machine sizeSized in cores deliberately, minimum fourCores are billed twice, as compute and as SQL Server license, and the license has a four-core floor. Dropping below four vCPUs saves compute and nothing on licensing.
AvailabilityAvailability zone for productionZonal placement carries the highest infrastructure commitment. Remember what it does not cover: the SQL Server process itself has no service level agreement, and building an availability group does not add one.

Step 2: Build the storage correctly, before SQL Server touches it

This is the step that decides whether the migrated database is faster or slower than the box it left, and it is the step most often skipped because the portal will happily build a machine without it.

SettingValueWhy
Data volume host cachingRead-onlyReads are served from the host cache. This is the single highest-value caching decision on the machine.
Log volume host cachingNoneWrite caching on a transaction log is a durability risk and a performance one. Never enable it.
tempdb locationThe ephemeral local disk, conventionally D:tempdb does not need to survive a reboot and the local disk is faster than anything you can attach. The IaaS Agent extension can configure this for you.
NTFS allocation unit size64 KB on data and log volumesMatches the engine’s extent size. Set at format time; changing it later means recreating the volume.
Disk typePremium SSD for data and log as the general answer; Premium SSD v2 where the machine series supports it and the workload justifies tuning IOPS separately from capacity; Ultra Disk for a log that genuinely needs sub-millisecond latencyPremium SSD v2 is attractive but in the portal it is still scoped to the Ebdsv5 and Ebsv5 series only, is available on new virtual machines only, requires an availability zone, cannot be mixed with other disk types, is not offered by the CLI or PowerShell at all, and its page still carries a preview label. Note also that it does not support host caching, so the read-only caching advice above does not apply to it. Treat it as a conditional choice rather than the new default. Ultra Disk is a measurement-driven decision, not an aspirational one.

Step 3: Register the IaaS Agent extension and set the license type

A SQL Server virtual machine that is not registered with the extension is invisible to every Azure management surface that would otherwise tell you it exists, is unpatched, or is misconfigured. A machine built from a marketplace image registers itself; a machine you built yourself or migrated in may not. Register everything, as an inventory discipline rather than a feature choice.

az sql vm create \
  --name vm-sql-fin-prod-01 \
  --resource-group rg-vcio-data-prod \
  --location eastus \
  --license-type AHUB

az sql vm show \
  -n vm-sql-fin-prod-01 -g rg-vcio-data-prod \
  --query "{sqlLicense:sqlServerLicenseType, mgmt:sqlManagement}" -o table

Expected result: the resource exists and reports the license type you intended. Failure modes: registering a machine whose SQL Server is not actually installed, or is installed as an unsupported edition, fails in a way that reads like a permissions error; and the license type is silently pay-as-you-go if you omit the flag, which nobody notices because everything works and the invoice is somebody else’s problem. Check the value, do not assume it.

While you are here, deal with patching. The original Automated Patching feature is scheduled to retire on 17 September 2027, and the replacement is Azure Update Manager, which is better in one specific and important way: in Microsoft’s own words, “Unlike with Automated Patching, Azure Update Manager installs Cumulative Updates for SQL Server.” Enabling both is explicitly cautioned against, so pick one. If you configure patching on this machine, configure Update Manager, and if you inherit machines still on the old feature, moving them is an upgrade rather than a chore.

Step 4: The portal migration path

The guided experience is called SQL Server migration in Azure Arc, and its name tells you where to find it: it lives on the Arc-enabled SQL Server resource for the source machine, where you select Database migration under Migration, not in an Azure Migrate project. That is worth stating plainly because most people go looking in the wrong place. The virtual machine target reached general availability in June 2026; the managed instance target arrived earlier.

The flow presents four tiles and they map onto work you would otherwise do by hand: Assess source instance, Select target, Migrate data, and Monitor and cutover. The assessment is already running against the Arc-enabled source, which is where the readiness findings and the sizing recommendation come from. Select target either picks an existing registered SQL Server VM or provisions a new one. Migrate data offers exactly one method for this target, Migrate using backup and restore, staged through an intermediary Azure Blob Storage account. Monitor and cutover does what it says.

Three constraints decide whether this path is available to you at all, and they are worth checking before you invest in it. The source machine has to be Arc-enabled, so if it is not, that is a prerequisite rather than an option, and it is free and covered in the Arc article. The staging storage has to be in the same subscription and the same region as the target, and both the operator and the virtual machine’s managed identity need Storage Blob Data Reader on it. And the documented limits are the ones that apply. Four classes of server object do not come across: SQL Server Agent jobs, credentials, SSIS packages and server audit. A source that is a failover cluster instance is not supported. You can migrate at most a hundred databases to one target virtual machine, and the flow will not overwrite an existing database on the target. There is no online mode here; the single documented method is backup and restore.

That last point is the honest reason I still teach step 5. When this path works it is genuinely good and it collapses four tools into one surface. When something behaves unexpectedly, the answer is in the documentation for the mechanism underneath rather than for the portal, and knowing the manual path is what lets you diagnose it.

Step 5: The manual path, backup to URL

Long-established, works from any source that meets the version floor, and the one to use when the source is not Arc-enabled or the guided flow is not available in your region or subscription.

The floors matter. Backup to URL needs SQL Server 2012 Service Pack 1 Cumulative Update 2 at minimum. Anything before 2016 writes page blobs, capped at one terabyte per backup. From 2016 onward you write block blobs authenticated with a shared access signature, and you can stripe across multiple URLs, which is the only way to move a large database this way. The numbers are worth knowing before you size the job: a single block blob holds roughly 195 gigabytes, being fifty thousand blocks at a four megabyte maximum transfer size, and striping across the documented maximum of sixty-four URLs raises the ceiling to 12.8 terabytes. Set the block size to 65536 and the maximum transfer size to 4194304, and stripe even modest backups, because differential and uncompressed backups exhaust the block limit well before they reach 195 gigabytes.

-- On the source instance
CREATE CREDENTIAL [https://stvciomig.blob.core.windows.net/backups]
  WITH IDENTITY = 'SHARED ACCESS SIGNATURE',
       SECRET = '<sas-token-without-leading-question-mark>';

BACKUP DATABASE [FIN]
  TO URL = 'https://stvciomig.blob.core.windows.net/backups/SQL2012-FIN01/FIN-20260721-2100-1.bak',
     URL = 'https://stvciomig.blob.core.windows.net/backups/SQL2012-FIN01/FIN-20260721-2100-2.bak',
     URL = 'https://stvciomig.blob.core.windows.net/backups/SQL2012-FIN01/FIN-20260721-2100-3.bak'
  WITH COPY_ONLY, COMPRESSION, CHECKSUM, STATS = 5;

Expected result: the striped blobs appear in the container and the backup reports completion. Failure modes: a shared access signature stored with its leading question mark fails authentication without saying why; a signature without write and list permissions fails midway through; and on a genuine 2012 source the credential form is different, because page blobs authenticate with the storage account key rather than a signature. If the source really is 2012, follow the page blob form and remember the one terabyte ceiling.

Then, on the new virtual machine, create the same credential and restore, this time with the file placement that a real instance requires and a managed instance does not:

RESTORE FILELISTONLY
  FROM URL = 'https://stvciomig.blob.core.windows.net/backups/SQL2012-FIN01/FIN-20260721-2100-1.bak',
       URL = 'https://stvciomig.blob.core.windows.net/backups/SQL2012-FIN01/FIN-20260721-2100-2.bak',
       URL = 'https://stvciomig.blob.core.windows.net/backups/SQL2012-FIN01/FIN-20260721-2100-3.bak';

RESTORE DATABASE [FIN]
  FROM URL = 'https://stvciomig.blob.core.windows.net/backups/SQL2012-FIN01/FIN-20260721-2100-1.bak',
       URL = 'https://stvciomig.blob.core.windows.net/backups/SQL2012-FIN01/FIN-20260721-2100-2.bak',
       URL = 'https://stvciomig.blob.core.windows.net/backups/SQL2012-FIN01/FIN-20260721-2100-3.bak'
  WITH MOVE 'FIN_Data' TO 'F:\sqldata\FIN.mdf',
       MOVE 'FIN_Log'  TO 'G:\sqllog\FIN.ldf',
       STATS = 5;

Run RESTORE FILELISTONLY first every time. It costs seconds and it proves the credential, the blobs and the logical file names before you commit to a long restore. Failure mode: omitting the MOVE clauses puts the files wherever the source had them, which on a machine you just built to a storage standard means the database lands on the wrong volumes and silently loses the benefit of everything you did in step 2.

For a near-zero-downtime move onto a virtual machine, the path is a distributed availability group instead, which works without an existing availability group at the source. Two constraints: the source needs to be SQL Server 2017 or later for a standalone instance, where 2016 or later suffices only when you already have an availability group to extend, and Microsoft’s prerequisites expect Enterprise edition at both ends. Below that version, or on Standard, this is an offline migration and the window has to be agreed. Note that the higher-level migration overview table still quotes a flat 2016 minimum for this method, so treat the prerequisites article as the governing reference.

Step 6: Hold the compatibility level and take the baseline

This is the step that separates a migration that lands from one that gets rolled back, and it takes about ten minutes.

SELECT name, compatibility_level, state_desc, recovery_model_desc
FROM sys.databases WHERE name = 'FIN';

-- Expect 110. Do not change it yet.

ALTER DATABASE [FIN] SET QUERY_STORE = ON;
ALTER DATABASE [FIN] SET QUERY_STORE (
  OPERATION_MODE = READ_WRITE,
  QUERY_CAPTURE_MODE = AUTO
);

Expected result: compatibility level 110 on a SQL Server 2022 instance whose own default is 160. The internal database version was upgraded irreversibly during the restore, and the compatibility level was preserved, which means the optimiser is still behaving as it did on the old box. That is the safety mechanism, and it is the reason you can move the platform without moving the query plans on the same night.

Let the workload run at 110 for a full business cycle with Query Store collecting. Then, as a separate scheduled and individually reversible change, raise the level and watch what moves. The cardinality estimator is where the trouble lives: plans that were stable for a decade get re-costed and a handful get materially worse. Because you have a before and an after, you can identify exactly which queries regressed and force the previous plan while each one is fixed properly. That workflow is what the Query Tuning Assistant exists to drive.

Worked example: a 2012 finance box onto a 2022 machine

The source: SQL2012-FIN01, Standard edition, 180 gigabytes, compatibility level 110, an application whose vendor certifies SQL Server up to 2022 and has never been asked about anything newer. The target chosen: SQL Server 2022 Standard on Windows Server 2022, four vCPUs, in an availability zone, with the license type set to AHUB because the client holds Standard licenses with Software Assurance.

The reasoning behind the two decisions that mattered. Standard edition rather than Enterprise, because nobody could name a feature that needed it, and Enterprise would have quadrupled the software line on the invoice. And SQL Server 2022 rather than 2025, purely because of the vendor’s certification, accepting that 2022 leaves less runway. That is the honest trade and it is worth writing into the design so that the next person understands it was a decision rather than an oversight.

The sequence on the night: application stopped, final copy-only backup striped across three blobs, restore with MOVE clauses onto the F and G volumes, compatibility level confirmed at 110, Query Store enabled, logins scripted across and orphans repaired, Agent jobs imported, application repointed and started. Total outage four hours against a six-hour window.

What actually went wrong, because something always does: the backup ran considerably slower than rehearsal because the rehearsal was taken from a source that had been restarted that morning and had a cold cache and an idle workload, while the real one ran at the end of a business day. Rehearse under production conditions, at the time of day the real cutover will happen, or your window is fiction.

The compatibility level was raised from 110 to 160 six weeks later, as its own change, on a Tuesday morning with the baseline in hand. Two queries regressed, both in the same overnight report. Both were pinned to their previous plans within the hour and rewritten properly over the following month. Nobody outside the database team noticed, which is exactly the outcome the discipline is for.

Step 7: Validate end to end

-- The database arrived intact and at the right level
SELECT name, compatibility_level, state_desc, page_verify_option_desc
FROM sys.databases WHERE name = 'FIN';

-- Files are on the volumes you built, not wherever the source had them
SELECT DB_NAME(database_id) AS db, name AS logical_name, physical_name
FROM sys.master_files WHERE database_id = DB_ID('FIN');

-- No orphaned users
EXEC sp_change_users_login 'Report';

-- Integrity, which is entirely yours on a virtual machine
DBCC CHECKDB ('FIN') WITH NO_INFOMSGS, ALL_ERRORMSGS;

Expected results: compatibility level 110; data and log files on the correctly-cached volumes; no rows from the orphaned users report; and a clean integrity check. That last one is not optional and is worth dwelling on. On the platform services Microsoft owns physical integrity and runs its own validation machinery. On a virtual machine it is entirely your responsibility, and the first run should happen before the application does, so that if the source had a problem you learn it now rather than inheriting it silently.

The end-to-end test: from an application host, run a full business transaction through the application against the new machine, confirm the overnight Agent job completes on its schedule, then verify that the machine’s backups are actually running and landing where you think they are. That last check catches the most common post-migration failure on infrastructure, which is a machine in production for a fortnight with no backup regime because everyone assumed the platform was doing it. On a virtual machine, nothing is doing it unless you configured it.

What comes next

You now own patching, backups, integrity checking and high availability on this machine, which is the bargain the doctrine article described. Configure Azure Update Manager, configure backups deliberately, schedule the integrity checks, and put the compatibility level raise in the calendar rather than leaving it to somebody’s memory. If the rest of the estate is not moving this year, the next build sheet Arc-enables it, which costs nothing and produces the assessment that makes the next decision on evidence.


Azure SQL
‹ Previous: [SQL 4] SQL Server on an Azure VM: Full Control, Full Bill
Next: [SQL 5] Arc-Enabled SQL Server: Managing What Stays