أتمتة اختبار استعادة Postgres من النسخ الاحتياطي أسبوعيًا
لو عندك pg_dump شغّال من 8 شهور وما جرّبتش restore ولا مرة، احتمال كبير يوم الكارثة تكتشف إن آخر نسخة corrupted أو ناقصة schema. خطوة تحقّق أسبوعية واحدة بتفرق بين ساعتين ترجع فيهم الموقع، وبين 48 ساعة مفقودة وعملاء غاضبين.
المشكلة باختصار
النسخ الاحتياطي اللي ما بيتجرّبش بيعاني من 3 أنواع من الفشل الصامت:
- الملف اتكتب على S3 لكن الـ stream اتقطع في النص والـ SHA256 ما اتحققش — الملف تالف وإنت مش عارف.
pg_dumpنجح لكن بإصدار Postgres مختلف عن الإنتاج، فالـ restore بيعمل errors على functions أو extensions زيpgcrypto.- الـ DB كبرت وبقى الـ restore بياخد 6 ساعات، وإنت مفترض إنه بياخد 20 دقيقة لأنك قسته أول يوم نشرت المشروع.
كل النقط دي مش هتظهرلك إلا يوم الكارثة — أو يوم تتجرّب restore فعلًا بإرادتك في بيئة معزولة.
مثال بسيط قبل ما نروح للكود
تخيّل إن عندك مفتاح احتياطي للشقة مُخبّى تحت حجر في الجنينة. بتطمئن إنه موجود كل 3 شهور بـ "أكيد لسه تحته". لحد اليوم اللي تتقفل فيه بره وتروح تدوّر عليه — وتلاقي جاركك كان خد الحجر من أسبوعين علشان ترتيب. المفتاح الاحتياطي اللي ما بتتأكّدش إنه بيفتح الباب فعلًا، مش مفتاح احتياطي — ده مجرد افتراض.
نفس الكلام بالظبط على الـ database backup. Backup Restore Drill هو إنك فعليًا تاخد الملف، تحطّه في بيئة معزولة، تعمل منه قاعدة شغّالة، وتتحقّق إن البيانات صحيحة. مش بس تقرأ log كاتب "Backup completed successfully".
إزاي الحل بيشتغل على GitHub Actions
الفكرة بسيطة: workflow بيتشغّل كل أحد الساعة 3 الصبح UTC. بيعمل الخطوات دي بالترتيب:
- بيسحب آخر dump من S3 (أو Backblaze B2 / Cloudflare R2) من bucket الـ backup.
- بيشغّل Postgres في container معزول — نفس إصدار الإنتاج بالظبط.
- بينفّذ
pg_restoreمن الـ dump على القاعدة المعزولة. - بيشغّل استعلامات تحقّق: row count لجداول حرجة، آخر تاريخ في جدول orders،
SELECT 1على materialized views. - بيبعت النتيجة على Slack مع وقت الاستعادة، حجم الملف، وأي warnings.
المثال التنفيذي — workflow كامل قابل للنسخ
name: backup-restore-test
on:
schedule:
- cron: '0 3 * * 0' # Sunday 03:00 UTC
workflow_dispatch:
jobs:
restore:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16.2
env:
POSTGRES_PASSWORD: testpass
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Install tools
run: |
sudo apt-get update -y
sudo apt-get install -y postgresql-client-16 awscli
- name: Pull latest dump from S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET }}
run: |
LATEST=$(aws s3 ls s3://myapp-backups/ | sort | tail -n 1 | awk '{print $4}')
aws s3 cp s3://myapp-backups/$LATEST ./backup.dump
echo "SIZE=$(du -h backup.dump | cut -f1)" >> $GITHUB_ENV
echo "FILENAME=$LATEST" >> $GITHUB_ENV
- name: Restore dump
run: |
START=$(date +%s)
PGPASSWORD=testpass pg_restore -h localhost -U postgres \
-d postgres --clean --if-exists --no-owner ./backup.dump
END=$(date +%s)
echo "DURATION=$((END-START))s" >> $GITHUB_ENV
- name: Verify integrity
run: |
PGPASSWORD=testpass psql -h localhost -U postgres -d postgres -c \
"SELECT COUNT(*) AS users FROM users;" | tee result.txt
PGPASSWORD=testpass psql -h localhost -U postgres -d postgres -c \
"SELECT MAX(created_at) FROM orders;" | tee -a result.txt
- name: Notify Slack
if: always()
run: |
STATUS="${{ job.status }}"
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"Backup restore test: $STATUS\nFile: $FILENAME\nSize: $SIZE\nDuration: $DURATION\"}" \
${{ secrets.SLACK_WEBHOOK }}
ركز على نقطة واحدة مهمة: الـ image postgres:16.2 لازم تطابق إصدار الإنتاج بالظبط. لو الإنتاج على 15.6 وإنت بتختبر على 16.2، ممكن الـ restore ينجح في التجربة ويفشل في الكارثة بسبب اختلاف في تعريف generated columns أو indexes.
الأرقام الفعلية من تجربتي
على dump حجمه 2.4 GB (حوالي 18 مليون row في 34 جدول):
- وقت السحب من S3
us-east-1: 38 ثانية. - وقت
pg_restore: 6 دقايق 12 ثانية. - وقت verify queries: أقل من ثانية.
- إجمالي الـ workflow: حوالي 8 دقايق.
- تكلفة GitHub Actions: ضمن الـ 2000 دقيقة المجانية على plan Free. شهريًا ~32 دقيقة فقط لـ 4 تشغيلات.
المفهوم بدقة: Recovery Time Objective
الـ RTO (Recovery Time Objective) هو أقصى وقت تقدر تستحمّله إن السيستم يفضل down. لو RTO عندك ساعة، والـ restore قياس فعلي بياخد 6 دقايق، إنت في أمان. لو الـ DB كبرت والـ restore بقى ساعة و40 دقيقة، خرجت من الـ RTO وإنت مش واخد بالك. الـ workflow ده بيرصد الـ trend كل أسبوع، فلو الـ duration بدأ يزيد، تحرّك قبل ما يبقى مشكلة إنتاج.
trade-offs لازم تعرفها
بتكسب: ثقة فعلية إن الـ backup شغّال، ورصد تدهور زمن الاستعادة مبكرًا، ودوكيومنتاشن مستمر لأدوات الـ restore.
بتخسر: الـ workflow مش بيتحقق من الـ data integrity الكاملة، بس بيعمل sanity checks. لو عايز قياس أعمق، زوّد استعلامات checksum على جداول مالية. الافتراض إن حجم الـ dump ≤ 10 GB — فوق كده GitHub Actions runner (بـ 7 GB RAM و 14 GB disk) مش مناسب ومحتاج instance مخصص.
متى لا تستخدم هذه الطريقة
لو الـ DB بتاعتك أكبر من 30 GB، سيّب GitHub Actions واعمل الـ drill على EC2 spot instance مؤقت بـ Terraform. السبب: runner بـ 7 GB RAM، والـ restore لـ DB كبيرة بياخد أكتر من ساعتين، وده هيقطعّك من limits GitHub وهيكلّفك وقت debug مش مستاهل.
كمان لو عندك PITR (Point-in-Time Recovery) بـ WAL archiving، الـ drill محتاج شكل تاني: restore base backup + apply WAL segments، مش pg_restore ملف واحد. في الحالة دي استخدم أدوات زي pgBackRest أو wal-g بدل الـ workflow البسيط ده.
الخطوة التالية
افتح repo الـ infra بتاعك دلوقتي وضيف الـ workflow تحت .github/workflows/backup-restore-test.yml. اضبط secrets: AWS_KEY, AWS_SECRET, SLACK_WEBHOOK. شغّله يدويًا مرة بـ workflow_dispatch قبل ما تسيبه للـ schedule. لو نجح، إنت عندك حاجز أمان أسبوعي أغلب الشركات مش عاملاه.