El almacenamiento es definitivamente una de las capacidades estrellas del cloud y Azure realmente brilla en ello, con una enorme flexibilidad tanto en características como modelos de facturación y niveles de servicio.

Las Storage Accounts de propósito general versión 2 y las de tipo blob tienen la interesante característica de que se puede definir la temperatura por cada archivo que almacenamos, estando los siguientes niveles disponbiles:

  • Acceso frecuente (hot). Precios balanceados entre almacenamiento, operaciones de lectura y escritura. Es ideal para información a la que accedemos y utilizamos con frecuencia.
  • Acceso esporádico (cool). El precio de almacenamiento es sensiblemente inferior (aproximadamente la mitad del hot), pero mayor en las operaciones de lectura y escritura.
  • Archivado. El precio de almacenamiento es extremadamente bajo (aproximadamente 9 veces inferior al hot), mientras que el de las operaciones de lectura es extremadamente alto. Ideal para almacenar datos a muy largo plazo los cuales sólo necesitan ser accedidos en situaciones muy especial, normalmente recuperación de desastres.

Es posible consultar aquí el detalle de precios de cada concepto. Aunque con la cantidad de conceptos, tipos de cuentas, redundancias y temperaturas de Azure Storage, la mejor forma de aclararse es ver la genial sesión de José Ángel Fernández e Iria Quiroja de la Global Azure Bootcamp 2018 - Storage Wars; son 55 minutos que nos ahorran varias horas buscando y leyendo documentación.

La modalidad Archive me llamó rápidamente la atención por su coste extremadamente de bajo de almacenamiento, dado que parece que tengo la carga de trabajo ideal.

Discos virtuales iSCSI

Una de las capacidades de Windows Server 2012 que ha pasado un poco desapercibido y que considero realmente útil es la implementación de soporte de discos virtuales (VHD y VHDX en Windows Server 2012 R2) en el servidor iSCSI.

En la red doméstica de servidor, tengo una máquina cuya función principal es servir espacio de almacenamiento por iSCSI que es consumido por otras máquinas de la red. Estos discos son fundamentalmente de datos. Actualmente el archivo más grande de este tipo que estoy sirviendo es un VHDX de 2 TB que hace las veces de NAS y puede alcanzar los 3 TB.

Como tengo toda una vida de documentos en ese archivo, me parece más que sensato hacerle una copia de seguridad en Azure. En lo primero en lo que podría pensar en es Azure Backup, pero a fecha de hoy no existe la temperatura de almacenamiento en este servicio (todo se considera hot) y sólo podemos elegir su redundancia. Sin embargo... ¿nos ahorramos tanto por elegir la temperatura? Un vistazo rápido a día de escribir estas líneas en la calculadora de Azure nos arroja esta información:

  • Región: West Europe
  • Tipo de almacenamiento: Block Blobs
  • Tipo de cuenta de almacenamiento: General Purpose v2
  • Redundancia: LRS
  • Capacidad: 2048 GB
  • Precio (sólo almacenamiento) hot: 33,85 EUR/mes
  • Precio (sólo almacenamiento) cool: 17,27 EUR/mes
  • Precio (sólo almacenamiento) archive: 3,89 EUR/mes

Por esa diferencia de precio y teniendo en cuenta que los archivos subidos prácticamente no los voy a tocar, diría que merece la pena hacer un pequeño script que haga el trabajo.

Volume Shadow Copy: diskshadow.exe

Si intento hacer cualquier operación con el archivo VHDX que estoy sirviendo por iSCSI me voy a encontrar un Access Denied en toda regla, dado que está en uso. La operación de carga a una Storage Account de Azure no es excepción.

A pesar de ser el afortunado poseedor de una conexión a Internet de fibra óptica con 600 Mbps de ancho de banda de subida, que me da aproximadamente un ratio de transferencia de 60 MB/s en el mejor de los casos. Los 2 TB de la imagen VHDX son equivalentes a 2.048 GB y 2.097.152 MB que a razón de 60 por segundo... nos sale que tardaría aproximadamente 10 horas en subir los 2 TB de información a Azure si la velocidad se resptea. Nada mal para una conexión doméstica sin ExpressRoute.

Aun así 10 horas es un tiempo de downtime inadmisible, así que no estoy por la labor de parar el servicio de iSCSI para hacer el backup. Afortunadamente los Volume Shadow Copy Services vienen al rescate, una tecnología que apareció por primera vez en Windows XP y Windows Server 2003 cuyo objetivo era justamente facilitar la realización de copias de seguridad consistentes incluso cuando los archivos se encuentran en uso.

Una de las formas más sencillas de interactuar con esta tecnología es la herramienta de línea de comandos diskshadow.exe, que es justo la que vamos a usar en el script.

¿Qué vamos a hacer?

Un script de PowerShell encargado de lo siguiente:

  1. Localiza el archivo del que queremos hacer la copia de seguridad y extrae la letra de la unidad.
  2. Construimos en tiempo real un mini-script de diskshadow.exe que indicará la unidad de la que vamos hacer un Shadow Copy
  3. Lanzamos diskshadow.exe y se nos crea una nueva letra de unidad, por defecto S:.
  4. Cargamos en Azure Storage el archivo utilizando la unidad Shadow como origen.
  5. Una vez cargado el archivo, cambiamos el tier del blob a Archive.
  6. Eliminamos archivos antiguos con cierta antigüedad (por defecto 181 días).
  7. Eliminamos la unidad Shadow que habíamos creado en el paso 3, llamando de nuevo a diskshadow.exe.

El script

No se hable más, sería el siguiente:

<#
.SYNOPSIS
    Creates a Volume Shadow Copy in order to backup and iSCSI server VHD volume that is currently in use, upload the file to an Storage Account and change it to Archive tier. This script must be run with Administrator privilege to access VSS.
.DESCRIPTION
    This script leverages the Azure Storage ARCHIVE tier for storing high volumes of data, such as VHD files served through iSCSI. As this could be troublesome due to the files being in use, this script takes the approach of using Volume Shadow Copy for allowing the operation without actual downtime of the disk.
.PARAMETER LogFile
    Path and filename of the operation log. It can be a UNC path.
    Example: "\\NAS\logs\iSCSI-backup.log"
.PARAMETER BlobContainerName
    Blob container name in the Azure Storage account.
.PARAMETER StorageAccountName
    Azure Storage account name.
.PARAMETER StorageAccountKey
    Key for accessing the storage account.
.PARAMETER BackupFile
    Path and filename of the the file to backup after VSS have been applied.
    Example: "S:\DATA.VHDX"
.PARAMETER DaysToRemoveOldObject
    Objects found in storage account older that this value will be removed
.PARAMETER ShadowDriveLetter
    Drive letter that should be use for Shadow Copy. It must not be in use by the system.
.EXAMPLE
    BackupISCSI-ToAzure.ps1 -BackupFile "E:\mymassivefile.vhdx" -StorageAccountName "backups" -StorageAccountKey "fh328wtjgewace098j3d39DSVMVCHNA9P" -BlobContainerName "iscsi" -LogFile "\\NAS\iSCSI-backup.log"
.LINK
    https://calnus.com
.NOTES
    Carlos Milán Figueredo
    https://www.calnus.com

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
    INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
    PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
    FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
    OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    DEALINGS IN THE SOFTWARE.
#>
param(
    [Parameter(Mandatory=$true)][string]$BackupFile,
    [Parameter(Mandatory=$true)][string]$StorageAccountName,
    [Parameter(Mandatory=$true)][string]$StorageAccountKey,
    [Parameter(Mandatory=$true)][string]$BlobContainerName,
    [string]$LogFile,
    [int]$DaysToRemoveOldObject=181,
    [string]$ShadowDriveLetter="S:",
    [string]$TempFolder="C:\Windows\TEMP"
)

$ErrorActionPreference = "Stop"
if(Get-Module -ListAvailable -Name "AzureRM.Storage") {
    Import-Module -Name AzureRM.Storage
} else {
    throw "AzureRM.Storage module not found"
}

$date = (Get-Date).ToString("yyyyMMdd")
$context = New-AzureStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $StorageAccountKey
$isOldDate = [DateTime]::UtcNow.AddDays(-$DaysToRemoveOldObject)
# $backupFileNameExtensionLess is just the filename without extension. Example: "myfile"
$backupFileNameExtensionLess = [System.IO.Path]::GetFileNameWithoutExtension($BackupFile)
# $backupFileNameExtension is the extension of the filename WITH dot. Example: ".vhdx"
$backupFileNameExtension = [System.IO.Path]::GetExtension($BackupFile)
# $backupFileNameDriveLetter is the driver letter followed by colon. Example: "E:"
$backupFileNameDriveLetter = (Get-Item $BackupFile) | Split-Path -Qualifier

# ##### DISKSHADOW SCRIPT #####
if($backupFileNameDriveLetter -eq $ShadowDriveLetter) {
    throw "Backup file and shadow file cannot share the same drive letter"
}
$DiskShadowScriptStart = "set context persistent nowriters
set verbose on
add volume $backupFilenameDriveLetter alias vssBackup
create
expose %vssBackup% $ShadowDriveLetter"
$DiskShadowScriptStop = "set verbose on
delete shadows exposed $ShadowDriveLetter"

if (Test-Path $(Split-Path -Path $LogFile -Parent) -PathType Container) {
    Start-Transcript -Path $LogFile -Force
}
Write-Output "File: $BackupFile"
Write-Output "Azure Storage Account: $StorageAccountName"
Write-Output "Blob Container: $BlobContainerName"
Write-Output "Days to remove old objects: $DaysToRemoveOldObject"
Write-Output ""

# ##### VOLUME SHADOW COPY CREATION #####
$DiskShadowScriptStart | Out-File -FilePath "$TempFolder\diskshadow-start.txt" -Encoding ascii
cmd.exe /c "diskshadow /s $TempFolder\diskshadow-start.txt"
Remove-Item -Path "$TempFolder\diskshadow-start.txt"
$BackupFileShadow = $BackupFile.Remove(0,2).Insert(0,$ShadowDriveLetter)

# ##### AZURE STORAGE UPLOAD & TIER SET #####
Set-AzureStorageBlobContent -Blob "$backupFileNameExtensionLess-$date$backupFilenameExtension" -Container $BlobContainerName -File $BackupFileShadow -Context $context -Force -Verbose
if($?) {
    $blob=Get-AzureStorageBlob -Blob "$backupFileNameExtensionLess-$date$backupFilenameExtension" -Container $BlobContainerName -Context $context -Verbose
    Write-Output "Setting storage tier to ARCHIVE..."
    $blob.ICloudBlob.SetStandardBlobTier("Archive")
    if($?) {
        Write-Output "Cleaning old files..."
        Get-AzureStorageBlob -Context $context -Container $BlobContainerName | Where-Object { $_.LastModified.UtcDateTime -lt $isOldDate -and $_.BlobType -eq "BlockBlob" } | Remove-AzureStorageBlob -Verbose
    }
}
# ##### VOLUME SHADOW COPY DELETION #####
$DiskShadowScriptStop | Out-File -FilePath "$TempFolder\diskshadow-stop.txt" -Encoding ascii
cmd.exe /c "diskshadow /s $TempFolder\diskshadow-stop.txt"
Remove-Item -Path "$TempFolder\diskshadow-stop.txt"
if (Test-Path $(Split-Path -Path $LogFile -Parent) -PathType Container) {
    Stop-Transcript
}

Creo que el script es fácil de entender de la mano de los comentarios, así que centrémonos en las partes clave:

  • Líneas 66-75. Introducimos en dos variables los scripts de creación y eliminación de las Shadow Copies.
  • Líneas 87-89. Volcamos el contenido de las variables en un archivo dentro de una carpeta temporal. Este archivo es el que llamamos con diskshadow.exe para crear la Shadow Copy. ¿Por qué lo hacemos por un archivo y no mediante un stream del pipeline? Pues porque sencillamente el programa no está preparado para ello, pero como véis tampoco supone problema.
  • Línea 93. Subimos el archivo desde la unidad Shadow a la Storage Account de Azure. Esta operación es la que en mi caso lleva 10 horas, pero podría ser fácilmente el doble. En cualquier caso ya no me preocupa porque no ocasiona interrupción del servicio.
  • Línea 97. Cambiamos el tier a Archive.
  • Líneas 104-106. Finalizada la operación, eliminamos el Shadow Copy que habíamos creado inicialmente.

¡Y eso ha sido todo!
Happy store!