لو قاعدة بياناتك وقعت دلوقتي، إنت خسرت كام ساعة شغل؟ السكربت اللي قدامك بياخد نسخة كاملة من PostgreSQL، يضغطها، يشفّرها، ويرفعها على S3 كل يوم الساعة 2 صباحًا. 40 سطر bash.
أتمتة نسخ احتياطي PostgreSQL إلى S3 يوميًا
المشكلة باختصار
أغلب الفرق الصغيرة بتعتمد على snapshot من مزوّد السيرفر (DigitalOcean, Hetzner, AWS) وبيفتكروا ده كفاية. مش كفاية. الـ snapshot بيتاخد على مستوى الـ volume مش على مستوى الـ transaction، فلو قاعدة البيانات كانت في نص عملية كتابة، النسخة ممكن ترجع corrupted. غير كده، لو محتاج تسترجع جدول واحد بس، الـ snapshot هيجبرك ترجع السيرفر كله.
الحل بسيط: pg_dump + gzip + gpg + aws s3 cp، مجدولين بـ cron يوميًا. التكلفة: أقل من دولارين شهريًا لقاعدة بيانات ≤ 5GB مع احتفاظ 30 يوم.
ليه pg_dump أفضل من snapshot الـ volume
خلّيني أوضّحها بمثال بسيط الأول. تخيّل إنك بتصوّر مكتب وفيه موظف بيكتب في ورقة. لو صوّرت الورقة وهو في نص الكتابة، هتلاقي كلمة ناقصة. ده اللي بيحصل في snapshot الـ volume: بيتاخد وقاعدة البيانات بتكتب، ساعات بتلاقي بلوك ناقص.
دلوقتي بالشرح الدقيق: pg_dump بيشتغل على الـ logical layer لـ PostgreSQL، مش على البلوكات على القرص. بيفتح transaction بـ isolation level REPEATABLE READ ويستفيد من الـ MVCC (Multi-Version Concurrency Control) علشان يقرأ snapshot consistent من غير ما يقفل الجداول. النتيجة: نسخة متناسقة (consistent) حتى وقاعدة البيانات شغالة تحت حمل كتابة.
الافتراض هنا: قاعدة بياناتك ≤ 50GB. لو أكبر، زمن الـ dump هيطوّل ويستهلك CPU وIO كتير. في الحالة دي لازم تفكّر في pg_basebackup مع WAL archiving بدل pg_dump.
السكربت الكامل
#!/usr/bin/env bash
set -euo pipefail
# === Config (خلّيها في متغيرات بيئة، مش hardcoded) ===
PG_HOST="${PG_HOST:-localhost}"
PG_USER="${PG_USER:-postgres}"
PG_DB="${PG_DB:-app_prod}"
S3_BUCKET="${S3_BUCKET:-my-app-backups}"
GPG_RECIPIENT="${GPG_RECIPIENT:-ops@example.com}"
RETENTION_DAYS="${RETENTION_DAYS:-30}"
TS=$(date -u +%Y%m%d_%H%M%S)
FILE="${PG_DB}_${TS}.dump.gz.gpg"
LOCAL="/tmp/${FILE}"
# === Dump + compress + encrypt في pipeline واحد ===
PGPASSWORD="${PG_PASSWORD}" pg_dump \
-h "$PG_HOST" -U "$PG_USER" -d "$PG_DB" \
--format=custom --no-owner --no-privileges \
| gzip -9 \
| gpg --batch --yes --encrypt --recipient "$GPG_RECIPIENT" \
> "$LOCAL"
# === Upload على S3 بـ storage class أرخص ===
aws s3 cp "$LOCAL" "s3://${S3_BUCKET}/daily/${FILE}" \
--storage-class STANDARD_IA
# === تحقّق إن الملف وصل فعلاً قبل ما تمسح المحلي ===
aws s3api head-object --bucket "$S3_BUCKET" --key "daily/${FILE}" > /dev/null
# === نضّف المحلي + النسخ الأقدم من retention ===
rm -f "$LOCAL"
CUTOFF=$(date -u -d "-${RETENTION_DAYS} days" +%Y-%m-%d)
aws s3 ls "s3://${S3_BUCKET}/daily/" \
| awk -v d="$CUTOFF" '$1 < d {print $4}' \
| xargs -I {} aws s3 rm "s3://${S3_BUCKET}/daily/{}"
echo "✓ backup ${FILE} uploaded, retention ${RETENTION_DAYS}d"