Upload and Restore SQL Managed Instance .bak from Azure Blob Storage
This technical guide provides a compact, repeatable flow for two operations:
- Upload a local SQL Server backup file (.bak) to Azure Blob Storage
- Restore the database from Blob Storage to Azure SQL Managed Instance
The scripts in this article are designed for operations teams that need predictable execution, explicit login steps, and minimal manual SSMS work. A typical Nodinite use is moving the Configuration Database or a Log Database from an on-premises SQL Server to Azure SQL Managed Instance; the full move, including the configuration changes afterwards, is in Database Migration.
Understanding the Restore Flow
Azure SQL Managed Instance can't read a file from your local disk, so the backup travels through Azure Blob Storage, and the managed instance reads it with a SAS credential.
local disk"] -->|"1. Upload, Az.Storage
HTTPS 443"| BLOB["fa:fa-box-archive Azure Blob Storage
container"] SAS["fa:fa-key SAS token
read and list"] -->|"2. CREATE CREDENTIAL"| MI["fa:fa-database Azure SQL Managed Instance"] BLOB -->|"3. RESTORE ... FROM URL"| MI MI -->|"4. Monitor with DMVs"| DMV["fa:fa-stopwatch sys.dm_exec_requests
sys.dm_operation_status"] style BAK fill:#2678BD,color:#fff style BLOB fill:#3F827B,color:#fff style SAS fill:#FFD525 style MI fill:#E47528,color:#fff style DMV fill:#93d16d
Diagram: upload the backup to Blob Storage (teal), create a SAS credential on the managed instance (yellow), restore from the URL (orange), and follow the asynchronous restore with DMVs (green). The network rules for reaching the managed instance from the Core Services Server are in SQL Server Firewall Configuration - Azure SQL Managed Instance.
What this article includes
- Azure login and subscription selection
- Upload script for local .bak to Blob Storage
- Restore script for Azure SQL Managed Instance using URL + SAS credential
- Validation and progress checks
- Condensed mapping to Microsoft Quickstart
Prerequisites
- PowerShell 7
- Network connectivity to Azure SQL Managed Instance endpoint; see SQL Server Firewall Configuration
- Permissions:
- Storage Blob Data Contributor on the target storage account (for upload)
- SQL permissions on managed instance to create credential and restore database
- Modules:
- Az.Accounts
- Az.Storage
- SqlServer
Install required modules:
Install-Module Az.Accounts -Scope CurrentUser -Repository PSGallery -Force
Install-Module Az.Storage -Scope CurrentUser -Repository PSGallery -Force
Install-Module SqlServer -Scope CurrentUser -Repository PSGallery -Force
Step 1 - Login to Azure
Connect-AzAccount -AuthScope Storage
Set-AzContext -Subscription "<subscription-id-or-name>"
Get-AzContext | Format-List Account, Subscription, Tenant, Environment
If your environment blocks popup-based auth:
Connect-AzAccount -AuthScope Storage -UseDeviceAuthentication
Set-AzContext -Subscription "<subscription-id-or-name>"
Step 2 - Upload local .bak to Blob Storage
Use the embedded script below.
#Requires -Version 7.0
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Subscription,
[Parameter(Mandatory = $true)]
[string]$StorageAccountName,
[Parameter(Mandatory = $true)]
[string]$ContainerName,
[Parameter(Mandatory = $true)]
[string]$LocalBakPath,
[string]$BlobName,
[int]$SasExpiryHours = 24,
[switch]$CreateContainerIfMissing
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
Import-Module Az.Accounts -ErrorAction Stop
Import-Module Az.Storage -ErrorAction Stop
if (-not (Test-Path -LiteralPath $LocalBakPath -PathType Leaf)) {
throw "Local .bak file not found: $LocalBakPath"
}
if ([string]::IsNullOrWhiteSpace($BlobName)) {
$BlobName = [System.IO.Path]::GetFileName($LocalBakPath)
}
Connect-AzAccount -AuthScope Storage | Out-Null
Set-AzContext -Subscription $Subscription | Out-Null
$ctx = New-AzStorageContext -StorageAccountName $StorageAccountName -UseConnectedAccount
$container = Get-AzStorageContainer -Name $ContainerName -Context $ctx -ErrorAction SilentlyContinue
if (-not $container) {
if (-not $CreateContainerIfMissing) {
throw "Container '$ContainerName' does not exist. Use -CreateContainerIfMissing to create it."
}
New-AzStorageContainer -Name $ContainerName -Context $ctx -Permission Off | Out-Null
}
Write-Host "Uploading '$LocalBakPath' to '$ContainerName/$BlobName'..."
$uploadParams = @{
File = $LocalBakPath
Container = $ContainerName
Blob = $BlobName
Context = $ctx
Force = $true
}
Set-AzStorageBlobContent @uploadParams | Out-Null
$expiry = (Get-Date).ToUniversalTime().AddHours($SasExpiryHours)
$containerSas = New-AzStorageContainerSASToken -Name $ContainerName -Context $ctx -Permission rl -ExpiryTime $expiry
$sasToken = $containerSas.TrimStart('?')
$blobUrl = "https://$StorageAccountName.blob.core.windows.net/$ContainerName/$BlobName"
$containerUrl = "https://$StorageAccountName.blob.core.windows.net/$ContainerName"
Write-Host "Upload completed."
Write-Host "BlobUrl: $blobUrl"
Write-Host "ContainerUrl: $containerUrl"
Write-Host "SasToken (no leading ?): $sasToken"
Write-Host "SasExpiryUtc: $($expiry.ToString('u'))"
Example:
# Save the embedded script to a local .ps1 file, then run it:
# pwsh .\Upload-BakToBlobStorage.ps1 ...
Expected output includes:
- Blob URL
- Container URL
- SAS token (without leading ?)
Save these values for restore.
Step 3 - Restore the database from Blob to Managed Instance
Use the embedded script below.
#Requires -Version 7.0
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ManagedInstanceFqdn,
[Parameter(Mandatory = $true)]
[string]$DatabaseName,
[Parameter(Mandatory = $true)]
[string]$BlobUrl,
[Parameter(Mandatory = $true)]
[string]$SasToken,
[Parameter(Mandatory = $true)]
[string]$SqlUser,
[Parameter(Mandatory = $true)]
[string]$SqlPassword,
[int]$QueryTimeoutSeconds = 0
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
Import-Module SqlServer -ErrorAction Stop
if ([string]::IsNullOrWhiteSpace($BlobUrl) -or -not $BlobUrl.StartsWith("https://")) {
throw "BlobUrl must be a valid https URL."
}
$sasTokenClean = $SasToken.TrimStart('?')
$lastSlash = $BlobUrl.LastIndexOf('/')
if ($lastSlash -lt 9) {
throw "BlobUrl must include container path and blob name."
}
$credentialUrl = $BlobUrl.Substring(0, $lastSlash)
$escapedCredentialUrl = $credentialUrl.Replace("'", "''")
$escapedSasToken = $sasTokenClean.Replace("'", "''")
$escapedBlobUrl = $BlobUrl.Replace("'", "''")
$escapedDatabaseName = $DatabaseName.Replace("]", "]]")
$sql = @"
IF EXISTS (SELECT 1 FROM sys.credentials WHERE name = N'$escapedCredentialUrl')
BEGIN
DROP CREDENTIAL [$escapedCredentialUrl];
END;
CREATE CREDENTIAL [$escapedCredentialUrl]
WITH IDENTITY = 'SHARED ACCESS SIGNATURE',
SECRET = '$escapedSasToken';
RESTORE FILELISTONLY
FROM URL = '$escapedBlobUrl';
RESTORE DATABASE [$escapedDatabaseName]
FROM URL = '$escapedBlobUrl';
"@
Write-Host "Starting restore for database '$DatabaseName' from '$BlobUrl'..."
$invokeParams = @{
ServerInstance = $ManagedInstanceFqdn
Database = "master"
Username = $SqlUser
Password = $SqlPassword
Query = $sql
QueryTimeout = $QueryTimeoutSeconds
}
Invoke-Sqlcmd @invokeParams | Out-Null
Write-Host "Restore command submitted."
Write-Host "Use the following query to monitor progress:"
Write-Host "SELECT session_id AS SPID, command, start_time, percent_complete FROM sys.dm_exec_requests WHERE command IN ('RESTORE DATABASE', 'BACKUP DATABASE');"
Example:
# Save the embedded script to a local .ps1 file, then run it:
# pwsh .\Restore-MiDatabaseFromBlob.ps1 ...
The script performs:
- CREATE CREDENTIAL for the Blob container URL
- RESTORE FILELISTONLY from the Blob URL
- RESTORE DATABASE from the Blob URL
- Progress query against sys.dm_exec_requests
Alternative Solution - T-SQL only (when .bak is already in Blob)
Use this path when you have already uploaded the backup file and only need restore operations in SSMS.
-- 1) Set values
DECLARE @ContainerUrl nvarchar(4000) = N'https://<storage>.blob.core.windows.net/<container>';
DECLARE @BlobUrl nvarchar(4000) = N'https://<storage>.blob.core.windows.net/<container>/<file>.bak';
DECLARE @SasToken nvarchar(max) = N'<sas-token-without-leading-question-mark>';
-- 2) Recreate credential
IF EXISTS (SELECT 1 FROM sys.credentials WHERE name = @ContainerUrl)
BEGIN
DECLARE @drop nvarchar(max) = N'DROP CREDENTIAL [' + @ContainerUrl + N'];';
EXEC(@drop);
END;
DECLARE @create nvarchar(max) =
N'CREATE CREDENTIAL [' + @ContainerUrl + N']
WITH IDENTITY = ''SHARED ACCESS SIGNATURE'',
SECRET = ''' + REPLACE(@SasToken, '''', '''''') + N''';';
EXEC(@create);
-- 3) Validate access to the backup
DECLARE @filelist nvarchar(max) =
N'RESTORE FILELISTONLY FROM URL = ''' + @BlobUrl + N''';';
EXEC(@filelist);
-- 4) Restore database
DECLARE @restore nvarchar(max) =
N'RESTORE DATABASE [NQA_Restored]
FROM URL = ''' + @BlobUrl + N''';';
EXEC(@restore);
Tip
On Azure SQL Managed Instance, do not use
WITH STATSin this restore statement. Use DMV queries to track progress.
Monitor restore progress
SELECT session_id AS SPID,
command,
start_time,
percent_complete,
DATEADD(second, estimated_completion_time / 1000, GETDATE()) AS estimated_completion_time
FROM sys.dm_exec_requests
WHERE command IN ('RESTORE DATABASE', 'BACKUP DATABASE');
SELECT operation,
major_resource_id,
state,
state_desc,
percent_complete,
error_code,
error_desc,
start_time,
last_modify_time
FROM sys.dm_operation_status
WHERE major_resource_id = 'NQA_Restored'
ORDER BY start_time DESC;
Condensed mapping to Microsoft Quickstart
This article condenses the SSMS-focused quickstart into script-first operations.
- Quickstart: Restore a database to Azure SQL Managed Instance with SSMS
- Use T-SQL to restore from a backup file
Key parity points with Microsoft guidance:
- Credential name must match the container path and use https
- IDENTITY must be SHARED ACCESS SIGNATURE
- SAS secret must not include leading ?
- Restore is asynchronous and can be monitored via DMVs
Common errors
StorageOAuthEndpointResourceId authentication failed
Run login again with storage auth scope:
Connect-AzAccount -AuthScope Storage
Set-AzContext -Subscription "<subscription-id-or-name>"
Error 3201 or Operating system error 86 during restore
Usually indicates credential/SAS mismatch. Recreate credential with correct container URL and SAS token without leading ?.
Msg 41901: option is not supported for this statement
This is typically caused by unsupported options (for example WITH STATS) on Azure SQL Managed Instance restore syntax. Remove the unsupported option and run plain RESTORE DATABASE ... FROM URL ...;.
No progress visible after starting restore
Restore is asynchronous. Keep monitoring with the DMV query in this article.
After the Restore
When a Nodinite database runs on the managed instance, point Nodinite at it:
- In the Portal, change the SQL Server connection string in Step 3: Infrastructure, and save the environment.
- Download the package and rerun the update with the CLI, as in Update Nodinite v7. The update writes the new connection string into the Core Services.
- Check the database locations that the Configuration Database itself stores, such as the Log Database entries, as described in Database Migration - Configuration Changes.
Next Steps
- Database Migration
- Database Migration - Configuration Changes
- SQL Server Firewall Configuration - Azure SQL Managed Instance