أتمتة اختبار استعادة 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 }}