December 5, 2025

Бэкап бакета S3 YandexCloud в локальное хранилище с помощью Veeam + PowerShell скриптов

Этот метод обходит ограничения Veeam, создавая локальную копию S3 данных, которую Veeam уже может бэкапить.

Полная архитектура решения

text

┌─────────────┐  скачивание  ┌─────────────┐  бэкап  ┌─────────────┐
│ Yandex S3   │─────────────▶│ Локальная   │─────────▶│ Veeam       │
│   бакет     │    AWS CLI   │  папка      │  Veeam  │  Repository │
└─────────────┘              └─────────────┘         └─────────────┘
       │                            │                       │
       └────────────────────────────┼───────────────────────┘
                                 PowerShell
                                 Pre-job Script

Шаг 1: Подготовка окружения

1.1 Установка AWS CLI на Windows

powershell

# Скачать и установить AWS CLI
# https://aws.amazon.com/cli/

# Проверить установку
aws --version

# Настройка профиля Yandex Cloud
aws configure set aws_access_key_id YOUR_KEY --profile yandex
aws configure set aws_secret_access_key YOUR_SECRET --profile yandex
aws configure set region ru-central1 --profile yandex
aws configure set s3.endpoint_url https://storage.yandexcloud.net --profile yandex

1.2 Создание структуры папок

powershell

# PowerShell - создать папки
New-Item -Path "C:\Backup\S3-Temp" -ItemType Directory -Force
New-Item -Path "C:\Backup\S3-Logs" -ItemType Directory -Force
New-Item -Path "C:\Scripts\Veeam" -ItemType Directory -Force

Шаг 2: Создание PowerShell скрипта

Полный скрипт с обработкой ошибок и логированием

powershell

# S3-Download-PreScript.ps1
# Сохранить в: C:\Scripts\Veeam\S3-Download-PreScript.ps1

param(
    [string]$JobName = $(throw "Job name is required"),
    [string]$BackupSource = $(throw "Backup source is required")
)

# Конфигурация
$Config = @{
    BucketName = "ваш-бакет"
    LocalPath = "C:\Backup\S3-Temp"
    LogPath = "C:\Backup\S3-Logs\$(Get-Date -Format 'yyyy-MM-dd')-sync.log"
    MaxRetries = 3
    RetryDelay = 30 # секунды
    AWSProfile = "yandex"
    ExcludePatterns = @("*.tmp", "*.temp", "*.log", "Thumbs.db")
}

# Функция логирования
function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $LogMessage = "$Timestamp [$Level] $Message"
    Add-Content -Path $Config.LogPath -Value $LogMessage
    Write-Host $LogMessage
}

# Функция скачивания с повторами
function Sync-S3Bucket {
    param([int]$RetryCount = 0)
    
    try {
        Write-Log "Начало синхронизации S3 бакета: $($Config.BucketName)"
        Write-Log "Целевая папка: $($Config.LocalPath)"
        
        # Формирование команды AWS CLI
        $ExcludeArgs = $Config.ExcludePatterns | ForEach-Object { "--exclude `"$_`"" }
        $ExcludeString = $ExcludeArgs -join " "
        
        $Command = "aws s3 sync s3://$($Config.BucketName) `"$($Config.LocalPath)`" " +
                   "--endpoint-url https://storage.yandexcloud.net " +
                   "--profile $($Config.AWSProfile) " +
                   "--no-progress " +
                   $ExcludeString
        
        Write-Log "Выполнение команды: $Command"
        
        # Выполнение команды
        $Result = Invoke-Expression $Command
        
        # Проверка результата
        if ($LASTEXITCODE -eq 0) {
            Write-Log "Синхронизация успешно завершена"
            
            # Сбор статистики
            $FileCount = (Get-ChildItem -Path $Config.LocalPath -Recurse -File | Measure-Object).Count
            $TotalSize = (Get-ChildItem -Path $Config.LocalPath -Recurse -File | Measure-Object -Property Length -Sum).Sum
            $SizeMB = [math]::Round($TotalSize / 1MB, 2)
            
            Write-Log "Статистика: $FileCount файлов, $SizeMB MB"
            return $true
        } else {
            throw "AWS CLI вернул ошибку: $LASTEXITCODE"
        }
    }
    catch {
        Write-Log "Ошибка при синхронизации: $_" -Level "ERROR"
        
        if ($RetryCount -lt $Config.MaxRetries) {
            Write-Log "Повторная попытка через $($Config.RetryDelay) секунд (попытка $($RetryCount + 1) из $($Config.MaxRetries))"
            Start-Sleep -Seconds $Config.RetryDelay
            return Sync-S3Bucket -RetryCount ($RetryCount + 1)
        } else {
            Write-Log "Превышено максимальное количество попыток" -Level "ERROR"
            return $false
        }
    }
}

# Функция очистки старых файлов
function Cleanup-OldFiles {
    param([int]$DaysToKeep = 7)
    
    try {
        Write-Log "Очистка файлов старше $DaysToKeep дней"
        
        $CutoffDate = (Get-Date).AddDays(-$DaysToKeep)
        $OldFiles = Get-ChildItem -Path $Config.LocalPath -Recurse -File | 
                    Where-Object { $_.LastWriteTime -lt $CutoffDate }
        
        $DeletedCount = 0
        foreach ($File in $OldFiles) {
            try {
                Remove-Item -Path $File.FullName -Force
                $DeletedCount++
            }
            catch {
                Write-Log "Не удалось удалить файл $($File.FullName): $_" -Level "WARNING"
            }
        }
        
        Write-Log "Удалено файлов: $DeletedCount"
        return $true
    }
    catch {
        Write-Log "Ошибка при очистке: $_" -Level "ERROR"
        return $false
    }
}

# Функция проверки свободного места
function Check-DiskSpace {
    param([string]$Path, [long]$RequiredSpaceMB = 1024)
    
    try {
        $Drive = (Get-Item $Path).Root.Name
        $DriveInfo = Get-PSDrive -Name $Drive.Substring(0,1)
        
        $FreeSpaceMB = [math]::Round($DriveInfo.Free / 1MB, 2)
        $RequiredSpaceFormatted = [math]::Round($RequiredSpaceMB, 2)
        
        Write-Log "Свободное место на диске $Drive : $FreeSpaceMB MB"
        
        if ($FreeSpaceMB -lt $RequiredSpaceMB) {
            Write-Log "ВНИМАНИЕ: Свободного места ($FreeSpaceMB MB) меньше требуемого ($RequiredSpaceFormatted MB)" -Level "WARNING"
            return $false
        }
        
        return $true
    }
    catch {
        Write-Log "Ошибка при проверке места на диске: $_" -Level "ERROR"
        return $false
    }
}

# Основной поток выполнения
try {
    Write-Log "========================================"
    Write-Log "Начало выполнения pre-job скрипта"
    Write-Log "Job Name: $JobName"
    Write-Log "Backup Source: $BackupSource"
    Write-Log "========================================"
    
    # 1. Проверка свободного места (минимум 10GB)
    if (-not (Check-DiskSpace -Path $Config.LocalPath -RequiredSpaceMB 10240)) {
        throw "Недостаточно свободного места на диске"
    }
    
    # 2. Синхронизация S3 бакета
    $SyncResult = Sync-S3Bucket
    if (-not $SyncResult) {
        throw "Синхронизация S3 не удалась"
    }
    
    # 3. Очистка старых файлов
    Cleanup-OldFiles -DaysToKeep 7
    
    # 4. Проверка, что файлы доступны для Veeam
    $TestFileCount = (Get-ChildItem -Path $Config.LocalPath -File -Recurse | Select-Object -First 10).Count
    if ($TestFileCount -eq 0) {
        Write-Log "ВНИМАНИЕ: В целевой папке нет файлов" -Level "WARNING"
    }
    
    Write-Log "Pre-job скрипт успешно завершен"
    exit 0
}
catch {
    Write-Log "Критическая ошибка в pre-job скрипте: $_" -Level "ERROR"
    exit 1
}

Шаг 3: Создание задания Veeam Backup Job

3.1 Настройка через Veeam Backup & Replication Console

text

1. Открыть Veeam Console
2. Backup Job → File Share → Microsoft Windows
3. Имя задания: "Yandex-S3-Backup"
4. На вкладке "Files and Folders":
   - Добавить → C:\Backup\S3-Temp
   - Исключить: *.tmp, *.log (дополнительно к скрипту)
5. На вкладке "Storage":
   - Repository: выберите локальный репозиторий
   - Retention policy: 30 restore points
   - Compression: Optimal
   - Deduplication: Enable (если есть)
6. На вкладке "Advanced":
   - Ссылка на скрипты → Scripts
   - Add pre-job script
   - Путь: C:\Scripts\Veeam\S3-Download-PreScript.ps1
   - Аргументы: -JobName "{JobName}" -BackupSource "{Source}"
7. На вкладке "Schedule":
   - Enable schedule
   - Daily at 02:00 AM
   - Days: Monday-Friday

3.2 Параметры скрипта в Veeam

powershell

# Veeam передает параметры в скрипт:
# {JobName} - имя задания Veeam
# {Source} - источник бэкапа
# {Target} - целевой репозиторий
# {ResultDir} - директория с результатами

# В нашем скрипте используем:
- JobName "{JobName}" 
- BackupSource "{Source}"

Шаг 4: Дополнительные скрипты

Post-job скрипт для очистки

powershell

# S3-Cleanup-PostScript.ps1
# Выполняется ПОСЛЕ успешного бэкапа Veeam

param(
    [string]$JobName,
    [string]$BackupSource,
    [string]$ResultDir
)

$Config = @{
    LocalPath = "C:\Backup\S3-Temp"
    LogPath = "C:\Backup\S3-Logs\$(Get-Date -Format 'yyyy-MM-dd')-cleanup.log"
    DaysToKeepInTemp = 3
}

function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $LogMessage = "$Timestamp [$Level] $Message"
    Add-Content -Path $Config.LogPath -Value $LogMessage
}

try {
    Write-Log "Начало post-job очистки"
    Write-Log "Задание: $JobName"
    
    # Очистка временных файлов старше N дней
    $CutoffDate = (Get-Date).AddDays(-$Config.DaysToKeepInTemp)
    $FilesToDelete = Get-ChildItem -Path $Config.LocalPath -Recurse -File | 
                     Where-Object { $_.LastWriteTime -lt $CutoffDate }
    
    $DeletedCount = 0
    $FailedCount = 0
    
    foreach ($File in $FilesToDelete) {
        try {
            Remove-Item -Path $File.FullName -Force
            $DeletedCount++
        }
        catch {
            Write-Log "Не удалось удалить $($File.FullName): $_" -Level "WARNING"
            $FailedCount++
        }
    }
    
    Write-Log "Удалено файлов: $DeletedCount, не удалось: $FailedCount"
    
    # Отправка отчета (опционально)
    if (Test-Path "$PSScriptRoot\Send-EmailReport.ps1") {
        & "$PSScriptRoot\Send-EmailReport.ps1" -JobName $JobName -Deleted $DeletedCount
    }
    
    Write-Log "Post-job скрипт завершен"
    exit 0
}
catch {
    Write-Log "Ошибка в post-job скрипте: $_" -Level "ERROR"
    exit 1
}

Скрипт для уведомлений

powershell

# Send-EmailReport.ps1
param(
    [string]$JobName,
    [int]$FilesDownloaded = 0,
    [int]$FilesDeleted = 0
)

$SmtpServer = "smtp.yandex.ru"
$SmtpPort = 587
$From = "backup@yourdomain.com"
$To = "admin@yourdomain.com"
$Credential = Get-Credential

$Body = @"
Отчет о бэкапе S3 бакета

Задание: $JobName
Время: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
Статус: УСПЕШНО

Статистика:
- Файлов скачано из S3: $FilesDownloaded
- Файлов очищено: $FilesDeleted
- Дисковое пространство: $((Get-PSDrive C).Free / 1GB) GB свободно

Лог файл: C:\Backup\S3-Logs\$(Get-Date -Format 'yyyy-MM-dd')-sync.log
"@

Send-MailMessage `
    -From $From `
    -To $To `
    -Subject "[Backup] S3 Backup Report - $JobName" `
    -Body $Body `
    -SmtpServer $SmtpServer `
    -Port $SmtpPort `
    -UseSsl `
    -Credential $Credential

Шаг 5: Настройка прав и безопасности

5.1 Сервисная учетная запись

powershell

# Создание учетной записи для Veeam
$Password = ConvertTo-SecureString "ComplexPassword123!" -AsPlainText -Force
New-LocalUser -Name "VeeamS3Backup" -Password $Password -FullName "Veeam S3 Backup Service" -Description "Service account for S3 backups"

# Добавление в группу
Add-LocalGroupMember -Group "Administrators" -Member "VeeamS3Backup"

# Настройка Veeam для использования этой учетной записи
# В свойствах задания Veeam → Advanced → Account

5.2 Безопасное хранение credentials

powershell

# Вариант 1: Windows Credential Manager
cmdkey /add:storage.yandexcloud.net /user:access_key /pass:secret_key

# Вариант 2: Зашифрованный файл
$Credential = Get-Credential
$Credential | Export-Clixml -Path "C:\Secure\aws-creds.xml"

# В скрипте используем:
$Credential = Import-Clixml -Path "C:\Secure\aws-creds.xml"

Шаг 6: Мониторинг и отчетность

Скрипт для проверки статуса

powershell

# Check-BackupStatus.ps1
$VeeamServer = "localhost"
$JobName = "Yandex-S3-Backup"

# Получение статуса из Veeam
# (Требуется Veeam PowerShell Module)
Import-Module Veeam.Backup.PowerShell

$Job = Get-VBRJob -Name $JobName
$LastSession = $Job.FindLastSession()

$Status = @{
    JobName = $Job.Name
    LastRun = $LastSession.CreationTime
    Result = $LastSession.Result
    ProcessedFiles = $LastSession.GetTaskSessions().Progress.ProcessedObjects
    ReadBytes = [math]::Round($LastSession.Progress.ReadSize / 1GB, 2)
    TransferredBytes = [math]::Round($LastSession.Progress.TransferedSize / 1GB, 2)
}

ConvertTo-Json $Status

Интеграция с системным мониторингом

powershell

# Для Zabbix/PRTG
$HealthFile = "C:\Backup\S3-Temp\.health"
$LastSync = (Get-ChildItem $HealthFile -ErrorAction SilentlyContinue).LastWriteTime

if ((Get-Date) - $LastSync -gt [TimeSpan]::FromHours(24)) {
    Write-Host "0"  # Проблема
} else {
    Write-Host "1"  # OK
}

Шаг 7: Оптимизация производительности

7.1 Настройка AWS CLI

ini

# C:\Users\Username\.aws\config
[profile yandex]
region = ru-central1
s3 =
    max_concurrent_requests = 20
    max_queue_size = 10000
    multipart_threshold = 64MB
    multipart_chunksize = 16MB

7.2 Оптимизация Veeam

text

В настройках задания Veeam:
- Storage → Advanced → Performance:
  * Compression level: Optimal
  * Storage optimization: LAN target
  * Enable inline deduplication: Yes
  
- Processing → Advanced → Advanced settings:
  * Enable CBT: Yes
  * Changed block tracking: Enable
  
- Maintenance:
  * Perform backup files health check: Monthly
  * Defragment and compact full backup file: Weekly

Проблемы и решения

Проблема 1: Таймауты при скачивании

powershell

# В скрипте добавляем параметры:
$Command = "aws s3 sync ... " +
           "--cli-read-timeout 600 " +
           "--cli-connect-timeout 300 " +
           "--cli-read-timeout 0 "

Проблема 2: Нехватка места на диске

powershell

# Автоматическая очистка по квоте
$QuotaGB = 100
$CurrentSizeGB = [math]::Round((Get-ChildItem $Config.LocalPath -Recurse | Measure-Object -Property Length -Sum).Sum / 1GB, 2)

if ($CurrentSizeGB -gt $QuotaGB) {
    # Удаляем самые старые файлы
    Get-ChildItem $Config.LocalPath -Recurse -File | 
        Sort-Object LastWriteTime |
        Select-Object -First 100 |
        Remove-Item -Force
}

Проблема 3: Конфликты блокировок файлов

powershell

# Проверка на заблокированные файлы
$LockedFiles = Get-ChildItem $Config.LocalPath -Recurse -File | 
               Where-Object { 
                   try { [IO.File]::Open($_.FullName, 'Open', 'Read', 'None') } 
                   catch { $true } 
               }

if ($LockedFiles) {
    Write-Log "Найдены заблокированные файлы: $($LockedFiles.Count)" -Level "WARNING"
}

Полная автоматизация через Task Scheduler

Если нужно независимое от Veeam расписание:

powershell

# Создание задачи в планировщике
$Action = New-ScheduledTaskAction -Execute "PowerShell.exe" `
    -Argument "-NoProfile -ExecutionPolicy Bypass -File `"C:\Scripts\Veeam\S3-Sync-Standalone.ps1`""

$Trigger = New-ScheduledTaskTrigger -Daily -At 1:30AM
$Principal = New-ScheduledTaskPrincipal -UserId "VeeamS3Backup" -LogonType Password -RunLevel Highest
$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries

Register-ScheduledTask -TaskName "S3-Sync-PreVeeam" `
    -Action $Action `
    -Trigger $Trigger `
    -Principal $Principal `
    -Settings $Settings

Итоговая структура файлов

text

C:\
├── Scripts\
│   └── Veeam\
│       ├── S3-Download-PreScript.ps1      # Основной скрипт
│       ├── S3-Cleanup-PostScript.ps1      # Скрипт очистки
│       ├── Send-EmailReport.ps1           # Уведомления
│       └── Check-BackupStatus.ps1         # Мониторинг
├── Backup\
│   ├── S3-Temp\                           # Временная копия S3
│   │   ├── file1.jpg
│   │   └── folder\
│   ├── S3-Logs\                           # Логи синхронизации
│   │   ├── 2024-01-15-sync.log
│   │   └── 2024-01-15-cleanup.log
│   └── VeeamRepository\                   # Репозиторий Veeam
└── Secure\
    └── aws-creds.xml                      # Зашифрованные ключи

Преимущества и недостатки этого подхода

✅ Преимущества:

  • Использует существующие инвестиции в Veeam
  • Централизованное управление и мониторинг
  • Дедупликация и компрессия Veeam
  • Retention policies Veeam
  • Интеграция с другими системами бэкапа

❌ Недостатки:

  • Двойное хранение данных (временная копия)
  • Сложность настройки
  • Зависимость от AWS CLI
  • Нет инкрементального скачивания S3→Local
  • Требует места для временной копии

📊 Когда использовать:

  • Уже есть Veeam Enterprise Plus
  • Нужна интеграция с существующей инфраструктурой
  • Требуется дедупликация для экономии места
  • Важен централизованный мониторинг

🚫 Когда НЕ использовать:

  • Только задача S3→Local (overkill)
  • Мало места для временной копии
  • Нет навыков PowerShell
  • Нужна реальная синхронизация (не по расписанию)