Backup de MySQL en Azure Utilizando Azure Automation

Introducción

La seguridad y la disponibilidad de los datos son elementos fundamentales en cualquier infraestructura tecnológica. En este artículo, detallaremos el proceso de configuración de un backup automatizado para una base de datos MySQL alojada en una máquina virtual (VM) Linux en Azure. Utilizaremos Azure Automation y una Identidad Administrada por Usuario (UMI) para lograr una autenticación segura y eficiente.

backup Incremental y Full

Una de las características destacadas de este script es su capacidad para realizar backups incrementales diarios, seguidos de un full  backup  los domingos. Esto se logra gracias a la configuración del Binary Log en MySQL, que permite identificar y respaldar solo las actualizaciones realizadas desde el último backup.

Personalización del Horario de backup

Si deseas modificar el horario de los backups incrementales, especialmente si no deseas realizar el full backup  solo los domingos, el script es altamente personalizable. Los parámetros dayOfWeek y dayfullbackup te permiten ajustar fácilmente estos horarios según tus necesidades.

Prerrequisitos

Antes de comenzar, es crucial configurar los permisos adecuados para la UMI en los servicios involucrados:

1. Permisos en la Cuenta de Almacenamiento (Storage Account)

Asegúrate de asignar el rol «Reader and Data Access» a la UMI en la cuenta de almacenamiento de Azure. Esto permitirá que la UMI acceda y realice operaciones en la cuenta de almacenamiento donde se guardarán los backups.

New-AzRoleAssignment -ObjectId <UMI_ObjectId> -RoleDefinitionName "Reader and Data Access" -Scope /subscriptions/<Subscription_Id>/resourceGroups/<Resource_Group_Name>/providers/Microsoft.Storage/storageAccounts/<Storage_Account_Name>

2. Permisos en Key Vault

La UMI también necesita permisos para acceder a los secretos almacenados en Azure Key Vault. Asigna el rol «Key Vault Secrets User» a la UMI en el Key Vault correspondiente.

Set-AzKeyVaultAccessPolicy -VaultName <Key_Vault_Name> -ObjectId <UMI_ObjectId> -PermissionsToSecrets get

3. Permisos en la Máquina Virtual (VM)

La UMI debe tener permisos específicos en la VM para ejecutar comandos y realizar operaciones necesarias. Utiliza un rol personalizado en la VM con acciones específicas.

{
    "Name": "Virtual Machine runCommand",
    "IsCustom": true,
    "Description": "Rol personalizado que permite ejecutar comandos en una máquina virtual",
    "Actions": [
        "Microsoft.Compute/locations/runCommands/read",
        "Microsoft.Compute/virtualMachines/runCommands/write",
        "Microsoft.Compute/virtualMachines/runCommand/action"
    ],
    "NotActions": [],
    "AssignableScopes": ["/subscriptions/<Subscription_Id>"]
}

Reemplaza <id-de-la-suscripcion> con el ID de la suscripción de Azure en la que deseas crear el rol. Este archivo JSON define un rol personalizado con tres acciones posibles: leer información de una máquina virtual, detener una máquina virtual y iniciar una máquina virtual.

Una vez que hayas creado el archivo JSON, ejecuta el siguiente comando para crear el rol personalizado en Azure:

az role definition create --role-definition createrole.json

Más información

4. Configuración de Binary Log en MySQL

Es esencial configurar el Binary Log en MySQL para realizar backups incrementales. Agrega las siguientes líneas al archivo de configuración de MySQL (my.cnf) en la VM:

log_bin = /var/log/mysql/backup/mysql-bin.log 
binlog_expire_logs_seconds = 604800
binlog_format = row 
max_binlog_size = 100M

Documentación adicional: MySQL Binary Log

Ejecución del Script

Antes de ejecutar el script, asegúrate de proporcionar los parámetros necesarios, incluidos SubscriptionId, ResourceGroupName, VmName, KeyvaultName, MysqlPasswordSecretName, StorageAccountName, ContainerBlodName, y MysqlUser.

Script

param (
    [Parameter(Mandatory=$true, HelpMessage="Introduzca el Id de la suscripcion donde se encuentra la VM")]
    [ValidateNotNullOrEmpty()]
    [String]$subscriptionId,

    [Parameter(Mandatory=$true, HelpMessage="Introduzca el nombre del grupo de recursos donde se encuentra la VM")]
    [ValidateNotNullOrEmpty()]
    [String]$resourceGroupName,

    [Parameter(Mandatory=$true, HelpMessage="Introduzca el nombre de la VM")]
    [ValidateNotNullOrEmpty()]
    [String]$vmName, 
   
    [Parameter(Mandatory=$true, HelpMessage="Introduzca el nombre del Keyvault")]
    [ValidateNotNullOrEmpty()]
    [String]$keyvaultName,

    [Parameter(Mandatory=$true, HelpMessage="Introduzca el nombre del secreto de la password MySQL en Key Vault")]
    [ValidateNotNullOrEmpty()]
    [String]$mysqlPasswordSecretName,

    [Parameter(Mandatory=$true, HelpMessage="Introduzca el nombre del Storage Account")]
    [ValidateNotNullOrEmpty()]
    [String]$storageAccountName,

    [Parameter(Mandatory=$true, HelpMessage="Introduzca el nombre del contenedor de Blob")]
    [ValidateNotNullOrEmpty()]
    [String]$containerBlodName,

     [Parameter(Mandatory=$true, HelpMessage="Introduzca el nombre del usuario de MysQL")]
     [ValidateNotNullOrEmpty()]
     [String]$mysqlUser

)

#Define variables

$authType = "Key"
$blobMountPath = "/mnt/blobfuse-tmp"
$containerName = "/etc/blobfuse/mysql"
$binaryLogs = "/var/log/mysql/backup"
#$Date =  (Get-Date).ToString("dd-MM-yyyy-HH-mm")
$Date = (Get-Date).AddHours(1).ToString("dd-MM-yyyy-HH-mm")
$dayOfWeek = $((Get-Date).DayOfWeek)
$dayfullbackup = "Sunday"

#region INITIALIZE CONTEXT VARIABLES & PREFERENCES
$global:InformationPreference = "Continue"
$global:ErrorActionPreference = "Stop"
Write-Information "##[$($MyInvocation.MyCommand)] Initializing Context Variables and Preferences"
#endregion

##Set Azure Context with user-assigned managed identity for MySqlServer
Write-Information "##[$($MyInvocation.MyCommand)] Setting Azure Context with user-assigned managed identity for MySqlServer."
$null = Disable-AzContextAutosave -Scope Process #Ensure that runbook don't inherit an AzContext
Connect-AzAccount -Identity -WarningAction SilentlyContinue -AccountId "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" -Scope Process -SkipContextPopulation

Write-Output "Suscripcion: '$((Get-AzSubscription -SubscriptionId $subscriptionId).Name)'"
Set-AzContext -Subscription $subscriptionId | Out-Null



#Simple error handling function
function Handle-Error {
    param (
        [string]$ErrorMessage
    )
    Write-Host "Error: $ErrorMessage" -ForegroundColor Red
    exit 1
}



#Check if Blobfuse is installed on the Linux VM.
$remotePath = ".\script.sh"  
$remoteCommand =
@"
check_blobfuse_installed() {
  if [ -x "/usr/bin/blobfuse" ]; then
    echo "blobfuse is installed."
    return 0
  else
    echo "blobfuse is not installed."
    return 1
  fi
}

if [ -f /etc/redhat-release ]; then
  echo "Detected Red Hat."

  # Check if blobfuse is installed
  if ! check_blobfuse_installed; then
    
    sudo rpm -Uvh https://packages.microsoft.com/config/rhel/8/packages-microsoft-prod.rpm
    sudo yum install blobfuse -y
  fi

elif [ -f /etc/SuSE-release ]; then
  echo "Detected SUSE."

  if ! check_blobfuse_installed; then
    
    sudo rpm -Uvh https://packages.microsoft.com/config/sles/15/packages-microsoft-prod.rpm
    sudo zypper install blobfuse -y
  fi

elif [ -f /etc/lsb-release ]; then
  echo "Detected Ubuntu."

  if ! check_blobfuse_installed; then
    
    sudo wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb
    sudo dpkg -i packages-microsoft-prod.deb
    sudo apt-get update
    sudo apt-get install blobfuse -y
  fi

else
  echo "Unsupported operating system."
  exit 1
fi
"@
 
#Save the command to a local file
Set-Content -Path $remotePath -Value $remoteCommand

#Convert the path to an absolute path and remove the drive letter
$absolutePath = (Convert-Path -Path $remotePath).Remove(0, 2)

#Invoke the command on the VM, using the relative path
(Invoke-AzVMRunCommand -Name $vmName -ResourceGroupName $resourceGroupName -CommandId 'RunShellScript' -ScriptPath $absolutePath).Value[0].Message  
Start-Sleep -Seconds 60

#Clean-up the local file
Remove-Item $remotePath



#Try to retrieve the storage account key and MySQL password
try {
    $mysqlPassword = (Get-AzKeyVaultSecret -VaultName $keyvaultName -Name $mysqlPasswordSecretName -AsPlainText)
    $storageAccount = Get-AzStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccountName
    $accountKey = (Get-AzStorageAccountKey -ResourceGroupName $resourceGroupName -Name $storageAccountName).Value[0]
} catch {
    Handle-Error -ErrorMessage "Error getting the storage account key"
}

#Check if Blobfuse is already configured and mount the file system

$remotePath = ".\script1.sh"  
$remoteCommand =
@"
if [ ! -d "/etc/blobfuse/" ]; then
    mkdir -p "/etc/blobfuse/"
    echo "Directory /etc/blobfuse/ created."
fi

if [ ! -d "$blobMountPath" ]; then
    mkdir -p "/mnt/blobfuse-tmp"
    echo "Directory /mnt/blobfuse-tmp created."
fi

if [ -f "$containerName" ]; then
    echo "The configuration file already exists in $containerName. The file system is mounted."
    blobfuse "$blobMountPath" --tmp-path=/mnt/resource/blobfusetmp --config-file="$containerName"
else
    
    echo "accountName $storageAccountName" > "$containerName"
    echo "accountKey $accountKey" >> "$containerName"
    echo "containerName $containerBlodName" >> "$containerName"
    echo "authType $authType" >> "$containerName"
    echo "Configuration file created in: $containerName."
    echo "File system is mounted at: $blobMountPath"
    blobfuse "$blobMountPath" --tmp-path=/mnt/resource/blobfusetmp --config-file="$containerName"
fi

"@
 
#Save the command to a local file
Set-Content -Path $remotePath -Value $remoteCommand

 #Convert the path to an absolute path and remove the drive letter
$absolutePath = (Convert-Path -Path $remotePath).Remove(0, 2)

#Invoke the command on the VM, using the relative path
(Invoke-AzVMRunCommand -Name $vmName -ResourceGroupName $resourceGroupName -CommandId 'RunShellScript' -ScriptPath $absolutePath).Value[0].Message  
Start-Sleep -Seconds 60

#Clean-up the local file
Remove-Item $remotePath



# full backup and incremental backup 

if ($dayOfWeek -eq $dayfullbackup) {
$remotePath = ".\script2.sh" 
$remoteCommand =
@"
if mount | grep -q "$blobMountPath"; then
    echo "Performing a Full Backup..."
    /usr/bin/mysqldump -u "$mysqlUser" -p"$mysqlPassword" --all-databases > "$blobMountPath/all-databases-full-backup-$Date.sql"
    echo "Full Backup completed: $blobMountPath/all-databases-full-backup-$Date.sql"
    umount -f "$blobMountPath"
fi
"@

#Save the command to a local file
Set-Content -Path $remotePath -Value $remoteCommand

 #Convert the path to an absolute path and remove the drive letter
$absolutePath = (Convert-Path -Path $remotePath).Remove(0, 2)

#Invoke the command on the VM, using the relative path
(Invoke-AzVMRunCommand -Name $vmName -ResourceGroupName $resourceGroupName -CommandId 'RunShellScript' -ScriptPath $absolutePath).Value[0].Message  
Start-Sleep -Seconds 60

#Clean-up the local file
Remove-Item $remotePath

} else {

$remotePath = ".\script4.sh" 
$remoteCommand =
@"
    /usr/bin/mysql -u "$mysqlUser" -p"$mysqlPassword" -e 'FLUSH LOGS;'
    /usr/bin/mysql -u "$mysqlUser" -p"$mysqlPassword" -e 'SHOW MASTER STATUS;'
"@

#Save the command to a local file
Set-Content -Path $remotePath -Value $remoteCommand

 #Convert the path to an absolute path and remove the drive letter
$absolutePath = (Convert-Path -Path $remotePath).Remove(0, 2)

#Invoke the command on the VM, using the relative path
$binlog_file = (Invoke-AzVMRunCommand -Name $vmName -ResourceGroupName $resourceGroupName -CommandId 'RunShellScript' -ScriptPath $absolutePath).Value[0].Message  
$binlog_file_value = [regex]::Match($binlog_file, 'mysql-bin\.\d+').Value
Start-Sleep -Seconds 60

#Clean-up the local file
Remove-Item $remotePath

Write-Host "The binary log is detected: $binlog_file_value"


$remotePath = ".\script5.sh" 
$remoteCommand =
@"
if mount | grep -q "$blobMountPath"; then
    echo "Performing an Incremental Backup using Binary Logs... $binlog_file_value"
    /usr/bin/mysqlbinlog --stop-never --to-last-log "$binaryLogs/$binlog_file_value" > "$blobMountPath/database-incremental-backup-$Date.sql"
    echo "Incremental Backup completed: $blobMountPath/database-incremental-backup-$Date.sql"
    umount -f "$blobMountPath"
fi
"@

#Save the command to a local file
Set-Content -Path $remotePath -Value $remoteCommand

 #Convert the path to an absolute path and remove the drive letter
$absolutePath = (Convert-Path -Path $remotePath).Remove(0, 2)

#Invoke the command on the VM, using the relative path
(Invoke-AzVMRunCommand -Name $vmName -ResourceGroupName $resourceGroupName -CommandId 'RunShellScript' -ScriptPath $absolutePath).Value[0].Message  
Start-Sleep -Seconds 60

#Clean-up the local file
Remove-Item $remotePath
}

Descripción Detallada del Script

El script PowerShell Backup-AzMySqlServer.ps1 se encarga de realizar backups automáticos de una base de datos MySQL en una VM de Azure. A continuación, desglosaremos las secciones más relevantes del script:

Sección de Parámetros (Param)

En esta sección, se definen los parámetros que el script espera recibir al ejecutarse. Estos incluyen información esencial como el ID de la suscripción, el nombre del grupo de recursos, el nombre de la VM, el nombre del Key Vault, el nombre del secreto de la contraseña de MySQL, etc.

param (
    [Parameter(Mandatory=$true, HelpMessage="Introduzca el Id de la suscripcion donde se encuentra la VM")]
    [ValidateNotNullOrEmpty()]
    [String]$subscriptionId,

    [Parameter(Mandatory=$true, HelpMessage="Introduzca el nombre del grupo de recursos donde se encuentra la VM")]
    [ValidateNotNullOrEmpty()]
    [String]$resourceGroupName,

    [Parameter(Mandatory=$true, HelpMessage="Introduzca el nombre de la VM")]
    [ValidateNotNullOrEmpty()]
    [String]$vmName, 
   
    [Parameter(Mandatory=$true, HelpMessage="Introduzca el nombre del Keyvault")]
    [ValidateNotNullOrEmpty()]
    [String]$keyvaultName,

    [Parameter(Mandatory=$true, HelpMessage="Introduzca el nombre del secreto de la password MySQL en Key Vault")]
    [ValidateNotNullOrEmpty()]
    [String]$mysqlPasswordSecretName,

    [Parameter(Mandatory=$true, HelpMessage="Introduzca el nombre del Storage Account")]
    [ValidateNotNullOrEmpty()]
    [String]$storageAccountName,

    [Parameter(Mandatory=$true, HelpMessage="Introduzca el nombre del contenedor de Blob")]
    [ValidateNotNullOrEmpty()]
    [String]$containerBlodName,

     [Parameter(Mandatory=$true, HelpMessage="Introduzca el nombre del usuario de MysQL")]
     [ValidateNotNullOrEmpty()]
     [String]$mysqlUser

)

Sección de Configuración del Contexto de Azure

Esta sección configura el contexto de Azure para el script. Asegura que las preferencias de información y manejo de errores estén configuradas correctamente. También establece la conexión a la cuenta de Azure utilizando la Identidad Administrada por Usuario (UMI) y selecciona la suscripción especificada.

$global:InformationPreference = "Continue"
$global:ErrorActionPreference = "Stop"
Write-Information "##[$($MyInvocation.MyCommand)] Initializing Context Variables and Preferences"

$null = Disable-AzContextAutosave -Scope Process #Ensure that runbook don't inherit an AzContext
Connect-AzAccount -Identity -WarningAction SilentlyContinue -AccountId "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" -Scope Process -SkipContextPopulation

Write-Output "Suscripcion: '$((Get-AzSubscription -SubscriptionId $subscriptionId).Name)'"
Set-AzContext -Subscription $subscriptionId | Out-Null

Obtención de Contraseña y Configuración de Blobfuse

En esta parte, el script intenta obtener la contraseña de MySQL desde Azure Key Vault y recupera la información de la cuenta de almacenamiento (nombre y clave) necesaria para la configuración de Blobfuse.

try {
    $mysqlPassword = (Get-AzKeyVaultSecret -VaultName $keyvaultName -Name $mysqlPasswordSecretName -AsPlainText)
    $storageAccount = Get-AzStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccountName
    $accountKey = (Get-AzStorageAccountKey -ResourceGroupName $resourceGroupName -Name $storageAccountName).Value[0]
} catch {
    Handle-Error -ErrorMessage "Error getting the storage account key"
}

Configuración de Blobfuse

Esta sección verifica si Blobfuse está instalado en la VM y lo instala si es necesario. Blobfuse es una herramienta que permite montar contenedores Blob de almacenamiento de Azure como sistemas de archivos locales en Linux.

$remotePath = ".\script.sh"  
$remoteCommand =
@"
check_blobfuse_installed() {
  if [ -x "/usr/bin/blobfuse" ]; then
    echo "blobfuse is installed."
    return 0
  else
    echo "blobfuse is not installed."
    return 1
  fi
}

if [ -f /etc/redhat-release ]; then
  echo "Detected Red Hat."

  # Check if blobfuse is installed
  if ! check_blobfuse_installed; then
    
    sudo rpm -Uvh https://packages.microsoft.com/config/rhel/8/packages-microsoft-prod.rpm
    sudo yum install blobfuse -y
  fi

elif [ -f /etc/SuSE-release ]; then
  echo "Detected SUSE."

  if ! check_blobfuse_installed; then
    
    sudo rpm -Uvh https://packages.microsoft.com/config/sles/15/packages-microsoft-prod.rpm
    sudo zypper install blobfuse -y
  fi

elif [ -f /etc/lsb-release ]; then
  echo "Detected Ubuntu."

  if ! check_blobfuse_installed; then
    
    sudo wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb
    sudo dpkg -i packages-microsoft-prod.deb
    sudo apt-get update
    sudo apt-get install blobfuse -y
  fi

else
  echo "Unsupported operating system."
  exit 1
fi
"@
 
#Save the command to a local file
Set-Content -Path $remotePath -Value $remoteCommand

#Convert the path to an absolute path and remove the drive letter
$absolutePath = (Convert-Path -Path $remotePath).Remove(0, 2)

#Invoke the command on the VM, using the relative path
(Invoke-AzVMRunCommand -Name $vmName -ResourceGroupName $resourceGroupName -CommandId 'RunShellScript' -ScriptPath $absolutePath).Value[0].Message  
Start-Sleep -Seconds 60

#Clean-up the local file
Remove-Item $remotePath

Creación de Copias de Seguridad

En función del día de la semana, el script decide si debe realizar un full backup o incremental utilizando Binary Logs. También extrae información del Binary Log para determinar el archivo específico que se utilizará en el backup incremental.


if ($dayOfWeek -eq $dayfullbackup) {
$remotePath = ".\script2.sh" 
$remoteCommand =
@"
if mount | grep -q "$blobMountPath"; then
    echo "Performing a Full Backup..."
    /usr/bin/mysqldump -u "$mysqlUser" -p"$mysqlPassword" --all-databases > "$blobMountPath/all-databases-full-backup-$Date.sql"
    echo "Full Backup completed: $blobMountPath/all-databases-full-backup-$Date.sql"
    umount -f "$blobMountPath"
fi
"@

#Save the command to a local file
Set-Content -Path $remotePath -Value $remoteCommand

 #Convert the path to an absolute path and remove the drive letter
$absolutePath = (Convert-Path -Path $remotePath).Remove(0, 2)

#Invoke the command on the VM, using the relative path
(Invoke-AzVMRunCommand -Name $vmName -ResourceGroupName $resourceGroupName -CommandId 'RunShellScript' -ScriptPath $absolutePath).Value[0].Message  
Start-Sleep -Seconds 60

#Clean-up the local file
Remove-Item $remotePath

} else {

$remotePath = ".\script4.sh" 
$remoteCommand =
@"
    /usr/bin/mysql -u "$mysqlUser" -p"$mysqlPassword" -e 'FLUSH LOGS;'
    /usr/bin/mysql -u "$mysqlUser" -p"$mysqlPassword" -e 'SHOW MASTER STATUS;'
"@

#Save the command to a local file
Set-Content -Path $remotePath -Value $remoteCommand

 #Convert the path to an absolute path and remove the drive letter
$absolutePath = (Convert-Path -Path $remotePath).Remove(0, 2)

#Invoke the command on the VM, using the relative path
$binlog_file = (Invoke-AzVMRunCommand -Name $vmName -ResourceGroupName $resourceGroupName -CommandId 'RunShellScript' -ScriptPath $absolutePath).Value[0].Message  
$binlog_file_value = [regex]::Match($binlog_file, 'mysql-bin\.\d+').Value
Start-Sleep -Seconds 60

#Clean-up the local file
Remove-Item $remotePath

Write-Host "The binary log is detected: $binlog_file_value"


$remotePath = ".\script5.sh" 
$remoteCommand =
@"
if mount | grep -q "$blobMountPath"; then
    echo "Performing an Incremental Backup using Binary Logs... $binlog_file_value"
    /usr/bin/mysqlbinlog --stop-never --to-last-log "$binaryLogs/$binlog_file_value" > "$blobMountPath/database-incremental-backup-$Date.sql"
    echo "Incremental Backup completed: $blobMountPath/database-incremental-backup-$Date.sql"
    umount -f "$blobMountPath"
fi
"@

#Save the command to a local file
Set-Content -Path $remotePath -Value $remoteCommand

 #Convert the path to an absolute path and remove the drive letter
$absolutePath = (Convert-Path -Path $remotePath).Remove(0, 2)

#Invoke the command on the VM, using the relative path
(Invoke-AzVMRunCommand -Name $vmName -ResourceGroupName $resourceGroupName -CommandId 'RunShellScript' -ScriptPath $absolutePath).Value[0].Message  
Start-Sleep -Seconds 60

#Clean-up the local file
Remove-Item $remotePath
}

:wq!

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *