补齐远程升级对象存储备份

This commit is contained in:
WuYongJie 2026-06-01 09:47:35 +08:00
parent fe11d65784
commit 77cc027b9e
4 changed files with 55 additions and 4 deletions

View File

@ -21,6 +21,8 @@ param(
[int]$AppPort = 4320,
[int]$RemoteHealthTimeoutSec = 120,
[switch]$NoRemoteDbBackup,
[switch]$NoRemoteObjectStorageBackup,
[string]$RemoteBackupHelperImage = "busybox:1.36",
[switch]$NoRollback,
[string]$RegistryHost = "",
@ -131,6 +133,8 @@ function Show-Help {
Write-Host " -UpdateRemoteEnv 兼容旧参数;DeployApp 现在默认会把 APP_IMAGE 等写入远程 .env"
Write-Host " -NoUpdateRemoteEnv 只让本次 compose 命令临时生效,不持久写入远程 .env"
Write-Host " -NoRemoteDbBackup 跳过远程 PostgreSQL 备份;默认会在升级前尝试备份 Compose 管理的数据库"
Write-Host " -NoRemoteObjectStorageBackup 跳过远程 MinIO 数据卷备份;默认会在升级前尝试备份 Compose 管理的素材文件"
Write-Host " -RemoteBackupHelperImage 远程备份对象存储卷时使用的 helper 镜像,默认 busybox:1.36"
Write-Host " -NoRollback 远程部署失败时不自动恢复旧 APP_IMAGE;默认启用失败回滚"
Write-Host " -RemoteHealthTimeoutSec 远程容器健康等待秒数,默认 120"
Write-Host " -DryRun 只打印将执行的动作,不实际执行"
@ -510,7 +514,9 @@ function New-RemoteDeployScript {
$quotedPostgresImage = ConvertTo-ShellSingleQuoted -Value (Get-RegistryImageFor -Repository 'runtime/postgres' -ImageTag '15-alpine')
$quotedMinioImage = ConvertTo-ShellSingleQuoted -Value (Get-RegistryImageFor -Repository 'runtime/minio' -ImageTag 'RELEASE.2025-04-22T22-12-26Z')
$quotedMinioMcImage = ConvertTo-ShellSingleQuoted -Value (Get-RegistryImageFor -Repository 'runtime/minio-mc' -ImageTag 'RELEASE.2025-04-16T18-13-26Z')
$quotedRemoteBackupHelperImage = ConvertTo-ShellSingleQuoted -Value $RemoteBackupHelperImage
$remoteBackupEnabled = if ($NoRemoteDbBackup) { "0" } else { "1" }
$remoteObjectBackupEnabled = if ($NoRemoteObjectStorageBackup) { "0" } else { "1" }
$rollbackEnabled = if ($NoRollback) { "0" } else { "1" }
$lines = New-Object System.Collections.Generic.List[string]
@ -527,6 +533,8 @@ function New-RemoteDeployScript {
$lines.Add("expected_version='$($RegistryImage.Split(':')[-1])'")
$lines.Add("remote_health_timeout=$RemoteHealthTimeoutSec")
$lines.Add("remote_db_backup_enabled=$remoteBackupEnabled")
$lines.Add("remote_object_backup_enabled=$remoteObjectBackupEnabled")
$lines.Add("remote_backup_helper_image=$quotedRemoteBackupHelperImage")
$lines.Add("rollback_enabled=$rollbackEnabled")
$lines.Add("export APP_IMAGE=$quotedRegistryImage")
$lines.Add("export NEXTAUTH_URL=$quotedNextAuthUrl")
@ -638,6 +646,39 @@ backup_postgres() {
echo "数据库备份完成:$db_backup"
}
backup_minio_volume() {
if [ "$remote_object_backup_enabled" != "1" ]; then
echo "已跳过远程对象存储备份"
printf '%s\n' "已按参数跳过对象存储备份。请确认外部 S3 / MinIO 或 Docker 卷已有独立备份。" > "$backup_dir/OBJECT_STORAGE_BACKUP_SKIPPED.txt"
return
fi
minio_id="$(docker compose -f "$compose_file" ps -q minio 2>/dev/null || true)"
if [ -z "$minio_id" ]; then
echo "未发现 Compose 管理的 minio 服务,可能使用外部对象存储;请确认已有外部对象存储备份"
printf '%s\n' "未发现 Compose 管理的 minio 服务。若使用外部 S3 / MinIO,请按客户现场方案单独备份对象存储。" > "$backup_dir/OBJECT_STORAGE_EXTERNAL.txt"
return
fi
minio_volume_name="$(read_env_value MINIO_VOLUME_NAME || true)"
if [ -z "$minio_volume_name" ]; then
minio_volume_name="flnxi-minio-data"
fi
if ! docker volume inspect "$minio_volume_name" >/dev/null 2>&1; then
echo "未找到 MinIO 数据卷:$minio_volume_name;请确认是否使用外部对象存储或自定义卷名"
printf '%s\n' "未找到 MinIO 数据卷:$minio_volume_name。若使用外部 S3 / MinIO,请按客户现场方案单独备份对象存储。" > "$backup_dir/OBJECT_STORAGE_VOLUME_MISSING.txt"
return
fi
object_backup="$backup_dir/minio-data-$deploy_started_at.tar.gz"
echo "执行对象存储备份:$object_backup"
docker run --rm -v "${minio_volume_name}:/source:ro" -v "$(pwd)/${backup_dir#./}:/backup" "$remote_backup_helper_image" sh -c 'cd /source && tar -czf /backup/minio-data.tar.gz .'
test -s "$backup_dir/minio-data.tar.gz" || { echo "对象存储备份失败:备份文件为空"; exit 32; }
mv "$backup_dir/minio-data.tar.gz" "$object_backup"
echo "对象存储备份完成:$object_backup"
}
wait_for_app_health() {
container_id="$1"
deadline=$((SECONDS + remote_health_timeout))
@ -684,6 +725,7 @@ verify_running_image() {
}
backup_postgres
backup_minio_volume
'@)
if (-not $NoRollback) {

View File

@ -216,12 +216,13 @@ docker logs flnxi-material --tail 100
- 升级前在远端创建 `backups/deploy-时间戳/`,保存升级前 `.env`。
- 如果远端 PostgreSQL 是当前 Compose 管理的 `postgres` 服务,会先执行 `pg_dump` 备份;如果使用外部数据库,会提示需要确认外部备份。
- 如果远端 MinIO 是当前 Compose 管理的 `minio` 服务,会先把 `MINIO_VOLUME_NAME` 对应的数据卷打包备份;如果使用外部 S3 / 外部 MinIO,会在备份目录写入提示文件,要求按客户现场方案单独备份对象存储。
- 部署失败时默认尝试恢复升级前 `.env`,并按旧运行镜像或旧 `APP_IMAGE` 重建应用容器。
- 部署后会在远端校验应用容器实际运行镜像、镜像内 `VERSION.txt` 和 Prisma 迁移状态,然后再做外部 `/api/health` 检查。
只有在已有其它备份和回滚体系时,才考虑使用 `-NoRemoteDbBackup` 或 `-NoRollback`。
只有在已有其它备份和回滚体系时,才考虑使用 `-NoRemoteDbBackup`、`-NoRemoteObjectStorageBackup` 或 `-NoRollback`。离线客户现场如果没有 `busybox:1.36`,需要先把该 helper 镜像放到目标机,或通过 `-RemoteBackupHelperImage` 指定现场已有的小型 Linux 镜像。
必须注意:脚本的自动回滚只恢复应用镜像和 `.env`,不自动恢复数据库。只要本次升级包含数据库迁移,升级前的数据库备份就必须保留,必要时由人工确认后恢复。
必须注意:脚本的自动回滚只恢复应用镜像和 `.env`,不自动恢复数据库和对象存储。只要本次升级包含数据库迁移或素材文件结构变化,升级前的数据库备份和对象存储备份就必须保留,必要时由人工确认后恢复。
典型用法:

View File

@ -131,7 +131,8 @@ Agent 升级时,新容器如果一直不健康,代码仍可能把结果当
当前处理:
- 已修复:发布脚本回滚输出和部署文档都明确“自动回滚只恢复应用镜像和 `.env`,不自动恢复数据库”;手动数据库恢复命令已补 `docker cp` 和标准输入两条可执行路径。
- 已修复:发布脚本回滚输出和部署文档都明确“自动回滚只恢复应用镜像和 `.env`,不自动恢复数据库和对象存储”;手动数据库恢复命令已补 `docker cp` 和标准输入两条可执行路径。
- 已加强:`deploy-registry.ps1 -Action DeployApp` 升级前默认不只备份 `.env` 和 Compose 管理的 PostgreSQL,也会尝试把 Compose 管理的 MinIO 数据卷打包到同一个 `backups/deploy-时间戳/` 目录;如果使用外部 S3 / 外部 MinIO,会写提示文件,要求按客户现场方案单独备份。
- 已补第二阶段产品化入口:`scripts/backup-restore.ps1` 支持 `Backup`、`Restore`、`Verify`。恢复必须显式传入 `RESTORE_FLNXI_DATA`,避免误覆盖客户数据;外部数据库和外部对象存储不会假装自动恢复。
### P0-08 生产镜像入口脚本在 Linux 容器内可能找不到

View File

@ -7,11 +7,18 @@ describe('deploy-registry.ps1', () => {
'utf8'
)
it('远程部署默认包含升级前数据库备份和 .env 备份', () => {
it('远程部署默认包含升级前数据库、对象存储和 .env 备份', () => {
expect(script).toContain('[switch]$NoRemoteDbBackup')
expect(script).toContain('[switch]$NoRemoteObjectStorageBackup')
expect(script).toContain('[string]$RemoteBackupHelperImage = "busybox:1.36"')
expect(script).toContain('env_backup="$backup_dir/.env.before-upgrade"')
expect(script).toContain("docker compose -f \"$compose_file\" exec -T postgres sh -c 'pg_dump")
expect(script).toContain('test -s "$db_backup"')
expect(script).toContain('backup_minio_volume()')
expect(script).toContain('docker volume inspect "$minio_volume_name"')
expect(script).toContain('tar -czf /backup/minio-data.tar.gz')
expect(script).toContain('OBJECT_STORAGE_EXTERNAL.txt')
expect(script).toContain('backup_minio_volume')
})
it('远程部署失败时默认会恢复旧 APP_IMAGE 或旧容器镜像', () => {