أتمتة Backup PostgreSQL إلى R2 مع اختبار Restore حقيقي
هتطلع من المقال بـ workflow عملي يعمل Backup PostgreSQL يومي، يرفعه إلى Cloudflare R2، ويختبر restore صغير قبل ما تعتبر النسخة صالحة. مستوى القارئ: متوسط.
المشكلة باختصار
الطريقة الشائعة بتقول: شغّل pg_dump وارفع الملف لأي storage. الطريقة دي بتفشل في نقطة مهمة: النسخة الاحتياطية لا تساوي شيء لو لم تختبر استعادتها. اللي بيحصل فعلاً إن الفريق يكتشف وقت العطل إن الملف ناقص، أو credentials غلط، أو dump قديم من قاعدة ثانية.
الافتراض هنا إن عندك PostgreSQL واحد، حجمه بين 1GB و80GB، وتحتاج نسخة يومية. لو عندك موقع SaaS صغير بـ 50K زائر يوميًا، فتعطل قاعدة البيانات لمدة ساعتين ممكن يوقف تسجيل الدخول والدفع والدعم. هدفنا مش disaster recovery كامل، هدفنا طبقة backup واضحة بتتكشف أخطاؤها بدري.
الفكرة الأساسية: backup لا ينجح إلا بعد restore
ركز في الفرق. pg_dump يعمل export منطقي لقاعدة واحدة. توثيق PostgreSQL يوضح أن pg_dump يأخذ نسخة متسقة حتى لو القاعدة عليها قراءة وكتابة، ولا يمنع المستخدمين من الوصول لها أثناء التشغيل. دي ميزة ممتازة لتطبيقات صغيرة ومتوسطة.
لكن pg_dump وحده لا يثبت إنك تعرف ترجع الخدمة. أفضل طريقة هي: dump بصيغة custom، رفع الملف، تنزيل آخر نسخة في بيئة مؤقتة، تشغيل pg_restore --list أو restore فعلي على قاعدة اختبار. الصيغة custom عبر -Fc مناسبة هنا لأنها مضغوطة افتراضيًا وتشتغل مع pg_restore.
Cloudflare R2 مناسب لأنه يدعم S3-compatible API، فالأدوات الموجودة مثل rclone أو AWS SDK تقدر تتعامل معه بتغيير endpoint. الـ trade-off هنا إنك تكسب بساطة وتوافق مع أدوات S3، لكن لازم تراجع حدود توافق S3 في R2 قبل استخدام features متقدمة مثل بعض خصائص object locking أو tagging.
خطوات التنفيذ
- أنشئ bucket في Cloudflare R2 باسم واضح مثل
prod-postgres-backups. - أنشئ API token بصلاحية Object Read & Write على هذا bucket فقط. بدل ما تعطي صلاحية على الحساب كله.
- أضف الأسرار في GitHub Secrets:
DATABASE_URL،R2_ACCOUNT_ID،R2_ACCESS_KEY_ID،R2_SECRET_ACCESS_KEY،R2_BUCKET. - شغّل workflow يومي في وقت ضغط قليل. مثلًا 02:30 UTC لو جمهورك الأساسي في الشرق الأوسط.
- بعد الرفع، نزّل نفس الملف وشغّل اختبار restore سريع على PostgreSQL service داخل GitHub Actions.
ملف GitHub Actions قابل للنسخ
هذا المثال يستخدم pg_dump وrclone. الرقم العملي: قاعدة 8GB غالبًا تتحول إلى dump مضغوط بين 1.5GB و3GB حسب نوع البيانات. على runner عادي، توقع 6 إلى 15 دقيقة. لو الوقت زاد عن 30 دقيقة يوميًا، راجع WAL archiving أو backup managed من مزود قاعدة البيانات.
name: postgres-r2-backup
on:
schedule:
- cron: '30 2 * * *'
workflow_dispatch:
jobs:
backup:
runs-on: ubuntu-latest
services:
restore-db:
image: postgres:16
env:
POSTGRES_PASSWORD: restore_pass
POSTGRES_DB: restore_check
ports:
- 5433:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Install tools
run: |
sudo apt-get update
sudo apt-get install -y postgresql-client rclone
- name: Create backup
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: |
set -euo pipefail
TS=$(date -u +%Y-%m-%dT%H-%M-%SZ)
echo "BACKUP_FILE=postgres-$TS.dump" >> $GITHUB_ENV
pg_dump "$DATABASE_URL" --format=custom --no-owner --file "postgres-$TS.dump"
ls -lh "postgres-$TS.dump"
- name: Configure rclone for R2
run: |
mkdir -p ~/.config/rclone
cat > ~/.config/rclone/rclone.conf <<EOF
[r2]
type = s3
provider = Cloudflare
access_key_id = ${{ secrets.R2_ACCESS_KEY_ID }}
secret_access_key = ${{ secrets.R2_SECRET_ACCESS_KEY }}
endpoint = https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
acl = private
EOF
- name: Upload backup to R2
run: |
rclone copy "$BACKUP_FILE" "r2:${{ secrets.R2_BUCKET }}/daily/" --checksum
- name: Restore smoke test
env:
PGPASSWORD: restore_pass
run: |
rclone copy "r2:${{ secrets.R2_BUCKET }}/daily/$BACKUP_FILE" ./restore-check --checksum
pg_restore --list "./restore-check/$BACKUP_FILE" | head -40
pg_restore --host localhost --port 5433 --username postgres --dbname restore_check --clean --if-exists "./restore-check/$BACKUP_FILE"
psql --host localhost --port 5433 --username postgres --dbname restore_check -c "select count(*) from information_schema.tables;"
ما الذي تكسبه وما الذي تدفعه
المكسب الأول إن الفشل يظهر بدري. لو token اتلغى، أو endpoint غلط، أو dump غير صالح، الـ workflow يفشل في نفس اليوم. المكسب الثاني إن R2 يستخدم endpoint بنمط S3: https://ACCOUNT_ID.r2.cloudflarestorage.com، فتقدر تغير الأداة لاحقًا بدون إعادة تصميم كاملة.
الـ trade-off هنا واضح. أنت تضيف 5 إلى 10 دقائق يوميًا لاختبار restore، وتستهلك مساحة تخزين إضافية. في المقابل، تقلل احتمال إن أول restore حقيقي يحصل تحت ضغط production. بدل ما تثق في وجود الملف، أنت تختبر قابلية استخدامه.
في فرق مهم بين backup وreplication. النسخة اليومية تحميك من حذف جدول بالغلط أو فساد بيانات اتشاف بعد ساعات. لكنها لا تعطيك RPO قريب من الصفر. لو مشروعك يقبل خسارة آخر 24 ساعة، هذا مناسب. لو لا، تحتاج PITR وWAL archiving أو خدمة managed backup من المزود.
متى لا تستخدم هذه الطريقة
لا تستخدمها كحل وحيد لو قاعدة البيانات أكبر من 100GB وتتغير بكثافة طوال اليوم. pg_dump منطقي ومفيد، لكنه يصبح بطيئًا ومكلفًا على قواعد ضخمة. لا تستخدمها أيضًا لو عندك أكثر من database وتحتاج roles وtablespaces؛ توثيق PostgreSQL يوضح أن pg_dump ينسخ قاعدة واحدة، بينما cluster كامل يحتاج أدوات أخرى مثل pg_dumpall أو استراتيجية physical backup.
كذلك، لا تعتمد عليها لو مطلوب منك retention قانوني مع immutability قوي. هنا تحتاج تصميم مختلف: lifecycle policy، صلاحيات فصل بين من يكتب ومن يحذف، وربما bucket lock حسب المزود.
المصادر
- توثيق PostgreSQL لـ pg_dump: يشرح النسخ المتسق، صيغة custom، وحدود نسخ قاعدة واحدة.
- توثيق Cloudflare R2 مع S3 tools: يوضح endpoint وصلاحيات Object Read & Write.
- توافق Cloudflare R2 مع S3 API: مهم قبل الاعتماد على ميزات S3 المتقدمة.
- توثيق GitHub Actions schedule: يوضح cron، UTC، وأقل تكرار للجدولة.
الخطوة التالية
الخطوة التالية: شغّل الـ workflow يدويًا مرة واحدة من workflow_dispatch، وافتح logs خطوة Restore smoke test. لو فشلت، لا تصلح الرفع الأول. أصلح restore الأول، لأن ده الاختبار اللي هينقذك وقت العطل.