- 7 minutes to read

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.

flowchart LR BAK["fa:fa-file .bak file
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:

  1. CREATE CREDENTIAL for the Blob container URL
  2. RESTORE FILELISTONLY from the Blob URL
  3. RESTORE DATABASE from the Blob URL
  4. 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 STATS in 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;

This article condenses the SSMS-focused quickstart into script-first operations.

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:

  1. In the Portal, change the SQL Server connection string in Step 3: Infrastructure, and save the environment.
  2. 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.
  3. 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