feat(scripts): add upload to S3 scripts
This commit is contained in:
15
.env.sample
15
.env.sample
@@ -1,3 +1,12 @@
|
|||||||
POSTGRES_DB=qlockify
|
POSTGRES_DB=qlockify
|
||||||
POSTGRES_USER=postgres
|
POSTGRES_USER=postgres
|
||||||
POSTGRES_PASSWORD=postgres
|
POSTGRES_PASSWORD=postgres
|
||||||
|
|
||||||
|
S3_BACKUP_BUCKET=
|
||||||
|
S3_BACKUP_PREFIX=qlockify
|
||||||
|
S3_BACKUP_REGION=us-east-1
|
||||||
|
S3_BACKUP_ENDPOINT_URL=
|
||||||
|
S3_BACKUP_ACCESS_KEY_ID=
|
||||||
|
S3_BACKUP_SECRET_ACCESS_KEY=
|
||||||
|
BACKUP_ENCRYPTION_PASSPHRASE=
|
||||||
|
BACKUP_LOCAL_KEEP_LATEST=1
|
||||||
|
|||||||
58
README.md
58
README.md
@@ -263,6 +263,64 @@ RESTORE_SKIP_MEDIA=1 ./scripts/restore.sh ./backups/qlockify-backup-YYYYMMDD-HHM
|
|||||||
RESTORE_SKIP_ENV=1 ./scripts/restore.sh ./backups/qlockify-backup-YYYYMMDD-HHMMSS.tar.gz
|
RESTORE_SKIP_ENV=1 ./scripts/restore.sh ./backups/qlockify-backup-YYYYMMDD-HHMMSS.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Install backup upload prerequisites:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y awscli openssl
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure encrypted S3-compatible backup uploads in `./.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
S3_BACKUP_BUCKET=qlockify-backups
|
||||||
|
S3_BACKUP_PREFIX=qlockify
|
||||||
|
S3_BACKUP_REGION=us-east-1
|
||||||
|
S3_BACKUP_ENDPOINT_URL=https://c284984.parspack.net
|
||||||
|
S3_BACKUP_ACCESS_KEY_ID=
|
||||||
|
S3_BACKUP_SECRET_ACCESS_KEY=
|
||||||
|
BACKUP_ENCRYPTION_PASSPHRASE=
|
||||||
|
BACKUP_LOCAL_KEEP_LATEST=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Upload an encrypted backup to S3:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ./scripts/backup-upload-s3.sh
|
||||||
|
./scripts/backup-upload-s3.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Restore the latest encrypted backup from S3:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ./scripts/restore-from-s3.sh
|
||||||
|
./scripts/restore-from-s3.sh latest
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Restore a specific encrypted backup from S3:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/restore-from-s3.sh qlockify/qlockify-backup-YYYYMMDD-HHMMSS.tar.gz.enc
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Schedule daily encrypted uploads with cron:
|
||||||
|
|
||||||
|
```cron
|
||||||
|
0 2 * * * cd /home/ubuntu/qlockify-deployment && ./scripts/backup-upload-s3.sh >> ./backups/backup.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
The S3 restore script supports the same partial restore flags:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
RESTORE_SKIP_DB=1 ./scripts/restore-from-s3.sh latest
|
||||||
|
RESTORE_SKIP_MEDIA=1 ./scripts/restore-from-s3.sh latest
|
||||||
|
RESTORE_SKIP_ENV=1 ./scripts/restore-from-s3.sh latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep `BACKUP_ENCRYPTION_PASSPHRASE` in a safe place outside the server. Encrypted backups cannot be restored without it.
|
||||||
|
|
||||||
## CI/CD with Gitea Actions
|
## CI/CD with Gitea Actions
|
||||||
|
|
||||||
This repository now ships with a Gitea Actions deployment workflow in:
|
This repository now ships with a Gitea Actions deployment workflow in:
|
||||||
|
|||||||
128
scripts/backup-upload-s3.sh
Normal file
128
scripts/backup-upload-s3.sh
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
backup-upload-s3.sh
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
DEPLOY_ROOT Deployment directory. Defaults to ~/qlockify-deployment.
|
||||||
|
S3_BACKUP_BUCKET S3 bucket name.
|
||||||
|
S3_BACKUP_PREFIX S3 object prefix. Defaults to qlockify.
|
||||||
|
S3_BACKUP_REGION S3 region. Defaults to us-east-1.
|
||||||
|
S3_BACKUP_ENDPOINT_URL Optional S3-compatible endpoint URL.
|
||||||
|
S3_BACKUP_ACCESS_KEY_ID S3 access key.
|
||||||
|
S3_BACKUP_SECRET_ACCESS_KEY S3 secret key.
|
||||||
|
BACKUP_ENCRYPTION_PASSPHRASE Passphrase used to encrypt backup archives.
|
||||||
|
BACKUP_LOCAL_KEEP_LATEST Set to 1 to keep latest encrypted archive locally.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[backup-s3] %s\n' "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf '[backup-s3] %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_var() {
|
||||||
|
local name="$1"
|
||||||
|
[[ -n "${!name:-}" ]] || fail "$name is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_env() {
|
||||||
|
local env_path="$1"
|
||||||
|
[[ -f "$env_path" ]] || fail "Deployment env file not found: $env_path"
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$env_path"
|
||||||
|
set +a
|
||||||
|
}
|
||||||
|
|
||||||
|
aws_s3() {
|
||||||
|
if [[ -n "${S3_BACKUP_ENDPOINT_URL:-}" ]]; then
|
||||||
|
aws --endpoint-url "$S3_BACKUP_ENDPOINT_URL" s3 "$@"
|
||||||
|
else
|
||||||
|
aws s3 "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_prefix() {
|
||||||
|
local value="${1:-qlockify}"
|
||||||
|
value="${value#/}"
|
||||||
|
value="${value%/}"
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v aws >/dev/null 2>&1 || fail "aws CLI is required"
|
||||||
|
command -v openssl >/dev/null 2>&1 || fail "openssl is required"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DEPLOY_ROOT="${DEPLOY_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
||||||
|
DEPLOY_ENV="$DEPLOY_ROOT/.env"
|
||||||
|
BACKUP_SCRIPT="$SCRIPT_DIR/backup.sh"
|
||||||
|
|
||||||
|
[[ -x "$BACKUP_SCRIPT" ]] || fail "Backup script is not executable: $BACKUP_SCRIPT"
|
||||||
|
load_env "$DEPLOY_ENV"
|
||||||
|
|
||||||
|
S3_BACKUP_PREFIX="$(normalize_prefix "${S3_BACKUP_PREFIX:-qlockify}")"
|
||||||
|
S3_BACKUP_REGION="${S3_BACKUP_REGION:-us-east-1}"
|
||||||
|
|
||||||
|
require_var S3_BACKUP_BUCKET
|
||||||
|
require_var S3_BACKUP_ACCESS_KEY_ID
|
||||||
|
require_var S3_BACKUP_SECRET_ACCESS_KEY
|
||||||
|
require_var BACKUP_ENCRYPTION_PASSPHRASE
|
||||||
|
|
||||||
|
export AWS_ACCESS_KEY_ID="$S3_BACKUP_ACCESS_KEY_ID"
|
||||||
|
export AWS_SECRET_ACCESS_KEY="$S3_BACKUP_SECRET_ACCESS_KEY"
|
||||||
|
export AWS_DEFAULT_REGION="$S3_BACKUP_REGION"
|
||||||
|
export AWS_EC2_METADATA_DISABLED=true
|
||||||
|
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||||
|
|
||||||
|
PLAIN_DIR="$WORK_DIR/plain"
|
||||||
|
mkdir -p "$PLAIN_DIR"
|
||||||
|
|
||||||
|
log "Creating local backup archive"
|
||||||
|
DEPLOY_ROOT="$DEPLOY_ROOT" "$BACKUP_SCRIPT" "$PLAIN_DIR"
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
archives=("$PLAIN_DIR"/*.tar.gz)
|
||||||
|
shopt -u nullglob
|
||||||
|
[[ "${#archives[@]}" -eq 1 ]] || fail "Expected exactly one backup archive, found ${#archives[@]}"
|
||||||
|
|
||||||
|
ARCHIVE_PATH="${archives[0]}"
|
||||||
|
ARCHIVE_NAME="$(basename "$ARCHIVE_PATH")"
|
||||||
|
ENCRYPTED_NAME="$ARCHIVE_NAME.enc"
|
||||||
|
ENCRYPTED_PATH="$WORK_DIR/$ENCRYPTED_NAME"
|
||||||
|
S3_URI="s3://$S3_BACKUP_BUCKET/$S3_BACKUP_PREFIX/$ENCRYPTED_NAME"
|
||||||
|
|
||||||
|
log "Encrypting backup archive"
|
||||||
|
openssl enc -aes-256-cbc -salt -pbkdf2 -iter 200000 \
|
||||||
|
-in "$ARCHIVE_PATH" \
|
||||||
|
-out "$ENCRYPTED_PATH" \
|
||||||
|
-pass env:BACKUP_ENCRYPTION_PASSPHRASE
|
||||||
|
|
||||||
|
rm -f "$ARCHIVE_PATH"
|
||||||
|
|
||||||
|
log "Uploading encrypted backup to $S3_URI"
|
||||||
|
aws_s3 cp "$ENCRYPTED_PATH" "$S3_URI" --only-show-errors
|
||||||
|
|
||||||
|
if [[ "${BACKUP_LOCAL_KEEP_LATEST:-1}" == "1" ]]; then
|
||||||
|
LATEST_DIR="$DEPLOY_ROOT/backups/latest"
|
||||||
|
mkdir -p "$LATEST_DIR"
|
||||||
|
find "$LATEST_DIR" -type f -name '*.tar.gz.enc' -delete
|
||||||
|
cp "$ENCRYPTED_PATH" "$LATEST_DIR/$ENCRYPTED_NAME"
|
||||||
|
log "Kept latest encrypted backup locally: $LATEST_DIR/$ENCRYPTED_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "S3 backup upload completed: $S3_URI"
|
||||||
137
scripts/restore-from-s3.sh
Normal file
137
scripts/restore-from-s3.sh
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
restore-from-s3.sh latest
|
||||||
|
restore-from-s3.sh <s3-object-key>
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
restore-from-s3.sh latest
|
||||||
|
restore-from-s3.sh qlockify/qlockify-backup-YYYYMMDD-HHMMSS.tar.gz.enc
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
DEPLOY_ROOT Deployment directory. Defaults to ~/qlockify-deployment.
|
||||||
|
S3_BACKUP_BUCKET S3 bucket name.
|
||||||
|
S3_BACKUP_PREFIX S3 object prefix. Defaults to qlockify.
|
||||||
|
S3_BACKUP_REGION S3 region. Defaults to us-east-1.
|
||||||
|
S3_BACKUP_ENDPOINT_URL Optional S3-compatible endpoint URL.
|
||||||
|
S3_BACKUP_ACCESS_KEY_ID S3 access key.
|
||||||
|
S3_BACKUP_SECRET_ACCESS_KEY S3 secret key.
|
||||||
|
BACKUP_ENCRYPTION_PASSPHRASE Passphrase used to decrypt backup archives.
|
||||||
|
|
||||||
|
Existing RESTORE_SKIP_DB, RESTORE_SKIP_MEDIA, and RESTORE_SKIP_ENV flags are
|
||||||
|
passed through to restore.sh.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[restore-s3] %s\n' "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf '[restore-s3] %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
require_var() {
|
||||||
|
local name="$1"
|
||||||
|
[[ -n "${!name:-}" ]] || fail "$name is required"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_env() {
|
||||||
|
local env_path="$1"
|
||||||
|
[[ -f "$env_path" ]] || fail "Deployment env file not found: $env_path"
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$env_path"
|
||||||
|
set +a
|
||||||
|
}
|
||||||
|
|
||||||
|
aws_s3() {
|
||||||
|
if [[ -n "${S3_BACKUP_ENDPOINT_URL:-}" ]]; then
|
||||||
|
aws --endpoint-url "$S3_BACKUP_ENDPOINT_URL" s3 "$@"
|
||||||
|
else
|
||||||
|
aws s3 "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_prefix() {
|
||||||
|
local value="${1:-qlockify}"
|
||||||
|
value="${value#/}"
|
||||||
|
value="${value%/}"
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
latest_object_key() {
|
||||||
|
aws_s3 ls "s3://$S3_BACKUP_BUCKET/$S3_BACKUP_PREFIX/" --recursive \
|
||||||
|
| awk '{print $4}' \
|
||||||
|
| grep '\.tar\.gz\.enc$' \
|
||||||
|
| sort \
|
||||||
|
| tail -n 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
REQUESTED_KEY="${1:-}"
|
||||||
|
[[ -n "$REQUESTED_KEY" ]] || {
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
command -v aws >/dev/null 2>&1 || fail "aws CLI is required"
|
||||||
|
command -v openssl >/dev/null 2>&1 || fail "openssl is required"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DEPLOY_ROOT="${DEPLOY_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
||||||
|
DEPLOY_ENV="$DEPLOY_ROOT/.env"
|
||||||
|
RESTORE_SCRIPT="$SCRIPT_DIR/restore.sh"
|
||||||
|
|
||||||
|
[[ -x "$RESTORE_SCRIPT" ]] || fail "Restore script is not executable: $RESTORE_SCRIPT"
|
||||||
|
load_env "$DEPLOY_ENV"
|
||||||
|
|
||||||
|
S3_BACKUP_PREFIX="$(normalize_prefix "${S3_BACKUP_PREFIX:-qlockify}")"
|
||||||
|
S3_BACKUP_REGION="${S3_BACKUP_REGION:-us-east-1}"
|
||||||
|
|
||||||
|
require_var S3_BACKUP_BUCKET
|
||||||
|
require_var S3_BACKUP_ACCESS_KEY_ID
|
||||||
|
require_var S3_BACKUP_SECRET_ACCESS_KEY
|
||||||
|
require_var BACKUP_ENCRYPTION_PASSPHRASE
|
||||||
|
|
||||||
|
export AWS_ACCESS_KEY_ID="$S3_BACKUP_ACCESS_KEY_ID"
|
||||||
|
export AWS_SECRET_ACCESS_KEY="$S3_BACKUP_SECRET_ACCESS_KEY"
|
||||||
|
export AWS_DEFAULT_REGION="$S3_BACKUP_REGION"
|
||||||
|
export AWS_EC2_METADATA_DISABLED=true
|
||||||
|
|
||||||
|
if [[ "$REQUESTED_KEY" == "latest" ]]; then
|
||||||
|
log "Resolving latest encrypted backup object"
|
||||||
|
OBJECT_KEY="$(latest_object_key)"
|
||||||
|
[[ -n "$OBJECT_KEY" ]] || fail "No encrypted backups found in s3://$S3_BACKUP_BUCKET/$S3_BACKUP_PREFIX/"
|
||||||
|
else
|
||||||
|
OBJECT_KEY="${REQUESTED_KEY#s3://$S3_BACKUP_BUCKET/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||||
|
|
||||||
|
ENCRYPTED_PATH="$WORK_DIR/$(basename "$OBJECT_KEY")"
|
||||||
|
DECRYPTED_PATH="$WORK_DIR/${ENCRYPTED_PATH##*/}"
|
||||||
|
DECRYPTED_PATH="${DECRYPTED_PATH%.enc}"
|
||||||
|
|
||||||
|
log "Downloading encrypted backup from s3://$S3_BACKUP_BUCKET/$OBJECT_KEY"
|
||||||
|
aws_s3 cp "s3://$S3_BACKUP_BUCKET/$OBJECT_KEY" "$ENCRYPTED_PATH" --only-show-errors
|
||||||
|
|
||||||
|
log "Decrypting backup archive"
|
||||||
|
openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \
|
||||||
|
-in "$ENCRYPTED_PATH" \
|
||||||
|
-out "$DECRYPTED_PATH" \
|
||||||
|
-pass env:BACKUP_ENCRYPTION_PASSPHRASE
|
||||||
|
|
||||||
|
log "Restoring decrypted backup archive"
|
||||||
|
DEPLOY_ROOT="$DEPLOY_ROOT" "$RESTORE_SCRIPT" "$DECRYPTED_PATH"
|
||||||
|
|
||||||
|
log "S3 restore completed from: s3://$S3_BACKUP_BUCKET/$OBJECT_KEY"
|
||||||
Reference in New Issue
Block a user