Cross-Tenant Azure Storage Archival

This guide describes how to use the Archive-StorageAccount.ps1 PowerShell 7 script to archive the contents of an Azure Storage Account from one tenant into an archive/backup Storage Account in a different tenant, using AzCopy and SAS tokens.


Overview

The script performs the following steps:

  1. Queries the source storage account for all blob containers via the Azure Blob REST API
  2. Creates a single destination container on the archive storage account, named after the source account
  3. Copies each source container recursively into a corresponding subfolder in the destination container
  4. Optionally deletes source data after a successful copy
  5. Logs all activity to a daily log file

Destination layout

Given a source account named mySourceAccount with containers data and backups, the result on the destination looks like this:

myarchiveaccount  (Storage Account)
└── mysourceaccount  (Container — named after the source account)
    ├── data/
    │   └── ... (all blobs from source container "data")
    └── backups/
        └── ... (all blobs from source container "backups")

Requirements

Requirement Details
PowerShell Version 7.0 or later
AzCopy Version 10+ in PATH or specify full path
Source SAS token Requires Read and List permissions on Blob service
Destination SAS token Requires Write and Create permissions on Blob service

Generating a SAS token (Azure Portal)

  1. Open your Storage Account in the Azure Portal
  2. Go to Security + networkingShared access signature
  3. Select the following:
    • Allowed services: Blob
    • Allowed resource types: Container + Object
    • Allowed permissions: Read, Write, List, Create (as appropriate)
    • Set a suitable expiry date
  4. Click Generate SAS and connection string
  5. Copy the SAS token (starts with ?sv=...)

Configuration

All configuration is done directly in the script file — no command-line arguments are needed. Open Archive-StorageAccount.ps1 and edit the top section:

# --- Source Storage Account ---
$SourceAccountName  = "mySourceAccount"
$SourceSasToken     = "?sv=2022-11-02&ss=b&..."

# --- Destination / Archive Storage Account ---
$DestAccountName    = "myArchiveAccount"
$DestSasToken       = "?sv=2022-11-02&ss=b&..."

# --- Behaviour ---
$AzCopyPath         = "azcopy"       # Full path e.g. "C:\Tools\azcopy.exe" if not in PATH
$OverwriteExisting  = $false         # true = overwrite files that already exist at destination
$DeleteAfterCopy    = $false         # true = delete source blobs after successful copy
$LogDirectory       = "$PSScriptRoot\Logs"

Warning: Set $DeleteAfterCopy = $true only when you have verified the archive is complete. This action is irreversible.


Running the Script

# Navigate to the script directory
cd C:\Scripts

# Run with PowerShell 7
pwsh .\Archive-StorageAccount.ps1

The script runs non-interactively and exits with code 0 on success or 1 if any container copy failed.


Logging

Logs are written to the Logs\ subfolder next to the script, one file per day:

Logs\
└── archive-20260325.log

Example log output:

[2026-03-25 14:00:01] [INFO]    AzCopy found: azcopy version 10.27.1
[2026-03-25 14:00:02] [INFO]    Fetching container list from: mySourceAccount
[2026-03-25 14:00:03] [INFO]    Found 2 container(s): data, backups
[2026-03-25 14:00:04] [INFO]    Ensuring destination container exists: 'mysourceaccount'
[2026-03-25 14:00:04] [INFO]    Container 'mysourceaccount' already exists -- continuing.
[2026-03-25 14:00:05] [INFO]    Starting copy: mySourceAccount/data --> myArchiveAccount/mysourceaccount/data
[2026-03-25 14:02:41] [SUCCESS] Copy completed successfully: mySourceAccount/data --> ...
[2026-03-25 14:02:42] [INFO]    Starting copy: mySourceAccount/backups --> ...
[2026-03-25 14:04:10] [SUCCESS] Copy completed successfully: mySourceAccount/backups --> ...
[2026-03-25 14:04:10] [INFO]    ========================================================
[2026-03-25 14:04:10] [INFO]    SUMMARY
[2026-03-25 14:04:10] [INFO]      [OK  ]  data --> mysourceaccount/data
[2026-03-25 14:04:10] [INFO]      [OK  ]  backups --> mysourceaccount/backups
[2026-03-25 14:04:10] [INFO]    Done: 2 succeeded, 0 failed.

Script Reference

Functions

Function Description
Write-Log Writes a timestamped, colour-coded log entry to console and log file
Assert-AzCopy Verifies AzCopy is available before proceeding
Get-StorageContainers Lists all containers in the source account via Blob REST API + regex parsing
Ensure-DestinationContainer Creates the archive container on the destination if it does not already exist (HTTP 409 = already exists, not an error)
Invoke-AzCopyTransfer Runs azcopy copy --recursive for a given source/destination URL pair

Why regex instead of XML parsing?

Invoke-RestMethod in PowerShell 7 pre-parses the XML response into a XmlDocument object. Casting this again with [xml] throws a type error. Invoke-WebRequest is used instead to retrieve the raw string, and container names are extracted with a simple regex pattern:

<Container><n>([^<]+)</n>

This is reliable because the Azure Blob List Containers response always places <n> as the first child element of <Container>.


Scheduling with Task Scheduler

To run this script on a schedule, create a Windows Task Scheduler task with the following action:

  • Program: pwsh.exe
  • Arguments: -NonInteractive -File "C:\Scripts\Archive-StorageAccount.ps1"
  • Start in: C:\Scripts

Ensure the task runs under an account with network access to both storage endpoints.


Download

Download Get-AzurePIMReport.zip{.button .button-primary}

Troubleshooting

AzCopy not found : Confirm AzCopy v10 is installed. Either add its directory to the system PATH or set $AzCopyPath to the full .exe path.

HTTP 403 on container list : The source SAS token is missing List permission or has expired. Regenerate the token.

HTTP 403 during copy : The destination SAS token is missing Write or Create permission, or the token has expired.

HTTP 409 on container creation : The destination container already exists — this is expected on subsequent runs and is handled gracefully.

No containers found : The script logs the first 500 characters of the raw API response. Check this output for error details returned by Azure (e.g. malformed SAS token).

AzCopy exit code 1 : Check the AzCopy journal/log files in %USERPROFILE%\.azcopy\ for detailed transfer errors.

Previous Post