Backup and Restore #
Tidak ada sistem yang kebal dari kehilangan data. Hardware gagal. Script migrasi berjalan dengan WHERE clause yang salah. Ransomware mengenkripsi seluruh disk. Developer menjalankan DELETE FROM orders di database produksi karena lupa mengganti connection string. Semua skenario ini nyata — dan semua pernah terjadi di sistem produksi yang dianggap sudah aman.
Bedanya antara insiden yang diselesaikan dalam hitungan jam dan bencana yang tidak bisa dipulihkan adalah satu hal: apakah backup ada, benar, dan pernah diuji restorenya.
Backup yang tidak pernah dicoba restore adalah ilusi keamanan. Kamu tidak tahu apakah backup itu valid sampai kamu benar-benar membutuhkannya — dan pada saat itu bukan waktu yang tepat untuk menemukan bahwa file backup corrupt atau prosedur restore-nya tidak terdokumentasi. Artikel ini membahas backup dan restore dari sisi yang sering dilewatkan: bukan hanya cara membuatnya, tapi cara memastikan ia benar-benar bekerja saat dibutuhkan.
Dua Konsep yang Harus Dipahami Sebelum Merancang Strategi #
Sebelum memutuskan strategi backup apa yang akan digunakan, ada dua konsep yang perlu didefinisikan terlebih dahulu karena mereka menentukan segalanya: RPO dan RTO.
RPO — Recovery Point Objective #
RPO adalah jawaban dari pertanyaan: “Berapa banyak data yang boleh hilang?” Lebih tepatnya — jika bencana terjadi sekarang, seberapa jauh ke belakang kamu bisa dan bersedia kembali?
Ilustrasi RPO:
Waktu: 08:00 12:00 16:00 20:00 ← bencana terjadi
│ │ │ │
Backup: [FULL] [incr] [incr] ✗ sistem down
Jika RPO = 4 jam:
→ Backup pukul 16:00 sudah cukup
→ Data antara 16:00–20:00 boleh hilang
Jika RPO = 1 jam:
→ Butuh backup lebih sering, atau continuous WAL/binlog shipping
→ Data tidak boleh hilang lebih dari 1 jam ke belakang
Semakin kecil RPO → semakin sering backup → semakin mahal
RPO bukan keputusan teknis — ini keputusan bisnis. Berapa nilai data yang dihasilkan dalam satu jam? Berapa kerugian jika satu jam transaksi harus diulang secara manual? Jawabannya menentukan seberapa agresif strategi backup harus dirancang.
RTO — Recovery Time Objective #
RTO adalah jawaban dari pertanyaan: “Berapa lama sistem boleh down saat pemulihan?” Ini adalah waktu maksimum yang diizinkan antara bencana terjadi dan sistem kembali beroperasi.
Ilustrasi RTO:
Bencana → Deteksi → Keputusan → Restore dimulai → Sistem online
terjadi (5 mnt) (10 mnt) (?) (?)
└──────────────────────┘
RTO
Jika RTO = 1 jam:
→ Restore harus selesai dalam ~45 menit (sisanya untuk deteksi dan keputusan)
→ Full restore dari mysqldump 100GB mungkin butuh 2 jam → tidak memenuhi RTO
→ Butuh physical backup atau standby database
Jika RTO = 4 jam:
→ Logical backup + restore mungkin sudah cukup
→ Lebih banyak pilihan strategi
Semakin kecil RTO → butuh solusi yang lebih cepat direstore → semakin mahal
Matriks strategi berdasarkan RPO dan RTO:
RPO / RTO │ RTO < 15 menit │ RTO < 1 jam │ RTO < 4 jam
───────────┼───────────────────┼───────────────────┼───────────────────
RPO < 1 mnt│ Standby DB + │ Physical backup │ Physical backup
│ PITR │ + WAL streaming │ + WAL/binlog
───────────┼───────────────────┼───────────────────┼───────────────────
RPO < 1 jam│ Physical backup │ Physical backup │ Logical backup
│ + WAL/binlog │ + incremental │ + incremental
───────────┼───────────────────┼───────────────────┼───────────────────
RPO < 1 hari│ Logical backup │ Logical backup │ Logical backup
│ harian │ harian │ harian
Lima Jenis Backup dan Kapan Menggunakannya #
Full Backup #
Full backup menyalin seluruh isi database pada satu titik waktu. Ini adalah fondasi dari semua strategi backup — hampir semua strategi lain bergantung pada full backup sebagai titik awal.
# Full logical backup dengan mysqldump (MySQL)
mysqldump \
--single-transaction \ # backup konsisten tanpa lock tabel (InnoDB)
--routines \ # sertakan stored procedure dan function
--triggers \ # sertakan trigger
--events \ # sertakan scheduled event
--hex-blob \ # binary data disimpan sebagai hex
-u root -p mydb \
| gzip > backup_full_$(date +%Y%m%d_%H%M%S).sql.gz
# Full logical backup dengan pg_dump (PostgreSQL)
pg_dump \
--format=custom \ # format custom: lebih compact, bisa parallel restore
--compress=9 \ # kompresi level 9
--verbose \
mydb > backup_full_$(date +%Y%m%d_%H%M%S).dump
# Full physical backup dengan Percona XtraBackup (MySQL)
xtrabackup \
--backup \
--target-dir=/var/backup/full \
--user=root \
--password=secret
Kapan digunakan: sebagai backup mingguan atau harian untuk database kecil sampai menengah. Untuk database besar (> 100GB), full backup sering dilakukan mingguan dan dikombinasikan dengan incremental untuk hari-hari lainnya.
Trade-off: full backup adalah yang paling mudah direstore, tapi paling lama dijalankan dan menghasilkan file terbesar.
Incremental Backup #
Incremental backup hanya menyalin perubahan yang terjadi sejak backup terakhir — entah full maupun incremental sebelumnya. Jauh lebih cepat dan lebih kecil dari full backup, tapi restorenya lebih kompleks karena butuh semua backup dalam rantai tersebut.
# Incremental backup dengan Percona XtraBackup (MySQL)
# Backup incremental pertama (basis: full backup)
xtrabackup \
--backup \
--target-dir=/var/backup/inc1 \
--incremental-basedir=/var/backup/full
# Backup incremental kedua (basis: incremental pertama)
xtrabackup \
--backup \
--target-dir=/var/backup/inc2 \
--incremental-basedir=/var/backup/inc1
Diagram rantai backup incremental:
Senin Selasa Rabu Kamis Jumat
[FULL] ← [INC1] ← [INC2] ← [INC3] ← [INC4]
Restore ke kondisi Kamis:
Restore FULL → Apply INC1 → Apply INC2 → Apply INC3 → selesai
(INC4 tidak diperlukan karena kita restore ke Kamis)
Jika INC2 corrupt → tidak bisa restore ke Kamis atau Jumat
→ Harus kembali ke FULL → kehilangan data Selasa dan Rabu
→ Ini risiko rantai panjang incremental
Rantai incremental yang panjang meningkatkan risiko: jika satu backup di tengah rantai corrupt, semua backup setelahnya tidak bisa digunakan. Strategi umum adalah membatasi rantai incremental maksimal 6 hari, lalu mulai lagi dengan full backup baru di hari ketujuh.
Differential Backup #
Differential backup menyalin semua perubahan sejak full backup terakhir — bukan sejak backup terakhir apapun. Ini berarti setiap differential backup semakin besar seiring waktu, tapi restorenya hanya butuh dua file: full backup + differential terakhir.
Perbandingan incremental vs differential:
Incremental:
Senin [FULL] → Selasa [+1MB] → Rabu [+1MB] → Kamis [+1MB]
Restore Kamis: FULL + inc_Selasa + inc_Rabu + inc_Kamis
Differential:
Senin [FULL] → Selasa [+1MB] → Rabu [+2MB] → Kamis [+3MB]
Restore Kamis: FULL + diff_Kamis (hanya dua file!)
Differential: restore lebih simpel, tapi ukuran backup makin besar tiap hari
Incremental: restore lebih kompleks, tapi ukuran backup tetap kecil tiap hari
Logical Backup #
Logical backup menghasilkan output berupa SQL statement atau format dump yang bisa dibaca manusia — CREATE TABLE, INSERT INTO, dan sebagainya. Ini adalah jenis backup yang paling portabel: bisa dipindahkan antar versi database, antar platform, bahkan diinspeksi isinya dengan text editor.
# Logical backup satu tabel saja (MySQL)
mysqldump --single-transaction mydb orders \
> backup_orders_$(date +%Y%m%d).sql
# Logical backup semua database (MySQL)
mysqldump --single-transaction --all-databases \
| gzip > backup_all_$(date +%Y%m%d).sql.gz
# Restore dari logical backup (MySQL)
gunzip < backup_full_20250601_020000.sql.gz | mysql -u root -p mydb
# Logical backup parallel dengan pg_dump (PostgreSQL — lebih cepat untuk DB besar)
pg_dump \
--format=directory \ # format direktori memungkinkan parallel restore
--jobs=4 \ # gunakan 4 worker parallel
--file=/var/backup/mydb \
mydb
# Restore parallel dengan pg_restore
pg_restore \
--dbname=mydb \
--jobs=4 \ # 4 worker parallel
--verbose \
/var/backup/mydb
Kapan digunakan: database kecil sampai menengah (< 50GB), kebutuhan portabilitas antar versi, atau ketika kamu perlu merestore hanya sebagian data (satu tabel, satu schema).
Keterbatasan: untuk database besar, mysqldump bisa memakan waktu jam. Juga tidak bisa digunakan untuk point-in-time recovery secara langsung.
Physical Backup #
Physical backup menyalin file-file fisik yang digunakan database: data files, log files, dan konfigurasi. Hasilnya adalah salinan eksak dari kondisi database pada titik waktu tertentu.
# Physical backup dengan Percona XtraBackup (MySQL — hot backup, tidak perlu lock)
xtrabackup --backup --target-dir=/var/backup/physical --user=root --password=secret
# Prepare backup sebelum restore (apply log yang belum di-commit)
xtrabackup --prepare --target-dir=/var/backup/physical
# Restore physical backup
# 1. Stop MySQL
systemctl stop mysql
# 2. Hapus data directory lama (hati-hati!)
rm -rf /var/lib/mysql/*
# 3. Copy backup ke data directory
xtrabackup --copy-back --target-dir=/var/backup/physical
# 4. Fix ownership
chown -R mysql:mysql /var/lib/mysql
# 5. Start MySQL
systemctl start mysql
Kapan digunakan: database besar (> 50GB), kebutuhan RTO kecil, atau sebagai basis untuk point-in-time recovery.
Point-in-Time Recovery: Kembali ke Detik yang Tepat #
Point-in-time recovery (PITR) memungkinkan kamu merestore database ke kondisi pada waktu yang sangat spesifik — bukan hanya ke titik backup terakhir, tapi ke detik tertentu sebelum insiden terjadi. Ini adalah kemampuan yang sangat berharga ketika human error terjadi dan kamu tahu persis kapan kesalahan itu dilakukan.
PITR bekerja dengan menggabungkan full backup dengan log perubahan yang direkam secara continuous:
Alur Point-in-Time Recovery:
Minggu 00:00 Senin 14:00 Senin 14:23
[FULL BACKUP] → ... semua perubahan ... → [DROP TABLE orders] ← insiden!
PITR ke Senin 14:22 (satu menit sebelum insiden):
Step 1: Restore full backup Minggu 00:00
Step 2: Apply binlog/WAL dari Minggu 00:00 sampai Senin 14:22:59
Step 3: Stop — jangan apply log setelah 14:22:59
Step 4: Database kembali ke kondisi tepat sebelum DROP TABLE dieksekusi
PITR dengan MySQL Binlog #
# Aktifkan binary logging di my.cnf (wajib untuk PITR)
# [mysqld]
# log_bin = /var/log/mysql/mysql-bin.log
# binlog_format = ROW
# expire_logs_days = 14
# Lihat daftar binlog yang tersedia
SHOW BINARY LOGS;
# Lihat isi binlog untuk mencari posisi insiden
mysqlbinlog /var/log/mysql/mysql-bin.000042 | grep -A5 "DROP TABLE"
# Restore: apply binlog sampai SEBELUM insiden (berdasarkan timestamp)
mysqlbinlog \
--start-datetime="2025-06-02 00:00:00" \
--stop-datetime="2025-06-02 14:22:59" \ # tepat sebelum insiden
/var/log/mysql/mysql-bin.000040 \
/var/log/mysql/mysql-bin.000041 \
/var/log/mysql/mysql-bin.000042 \
| mysql -u root -p mydb
# Atau berdasarkan posisi binlog (lebih presisi dari timestamp)
mysqlbinlog \
--start-position=4 \
--stop-position=156789 \ # posisi tepat sebelum statement yang bermasalah
/var/log/mysql/mysql-bin.000042 \
| mysql -u root -p mydb
PITR dengan PostgreSQL WAL #
# Konfigurasi WAL archiving di postgresql.conf
# wal_level = replica
# archive_mode = on
# archive_command = 'cp %p /var/backup/wal/%f'
# recovery.conf (PostgreSQL < 12) atau postgresql.conf (PostgreSQL >= 12)
# restore_command = 'cp /var/backup/wal/%f %p'
# recovery_target_time = '2025-06-02 14:22:59'
# recovery_target_action = 'promote'
Untuk PITR berhasil, binary log (MySQL) atau WAL archive (PostgreSQL) harus tersedia secara continuous sejak full backup terakhir. Jika ada gap dalam log — misalnya log dihapus sebelum backup terbaru diambil, atau disk log penuh dan log tidak bisa ditulis — PITR tidak bisa dilakukan ke titik waktu setelah gap tersebut. Pastikan monitoring log tersedia dan retensi log tidak lebih pendek dari interval full backup.
Strategi Backup Berlapis: 3-2-1 Rule #
Strategi paling teruji untuk backup adalah 3-2-1 rule: simpan 3 salinan data, di 2 media yang berbeda, dengan 1 salinan di lokasi yang berbeda secara fisik.
3-2-1 Backup Rule:
3 salinan:
┌──────────────────────────────────────────────────────────┐
│ Salinan 1: Database produksi (data live) │
│ Salinan 2: Backup di server backup (sama lokasi) │
│ Salinan 3: Backup di object storage remote (S3/GCS) │
└──────────────────────────────────────────────────────────┘
2 media berbeda:
┌──────────────────────────────────────────────────────────┐
│ Media 1: Disk server (lokal) │
│ Media 2: Object storage (cloud) │
└──────────────────────────────────────────────────────────┘
1 offsite:
┌──────────────────────────────────────────────────────────┐
│ Salinan di S3/GCS/Azure Blob berada di region berbeda │
│ Jika data center utama terbakar, backup tetap aman │
└──────────────────────────────────────────────────────────┘
Implementasi sederhana menggunakan cron dan AWS S3:
#!/bin/bash
# backup_and_upload.sh — jalankan via cron setiap hari pukul 02:00
BACKUP_DIR="/var/backup/db"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DB_NAME="myapp"
S3_BUCKET="s3://my-company-db-backups"
RETENTION_DAYS=7
# Step 1: buat backup lokal
mysqldump \
--single-transaction \
--routines --triggers --events \
-u backup_user -p"$DB_PASSWORD" \
"$DB_NAME" \
| gzip > "$BACKUP_DIR/backup_${TIMESTAMP}.sql.gz"
# Step 2: enkripsi sebelum upload
gpg --symmetric \
--cipher-algo AES256 \
--passphrase "$BACKUP_ENCRYPTION_KEY" \
"$BACKUP_DIR/backup_${TIMESTAMP}.sql.gz"
# Step 3: upload ke S3
aws s3 cp \
"$BACKUP_DIR/backup_${TIMESTAMP}.sql.gz.gpg" \
"$S3_BUCKET/daily/" \
--storage-class STANDARD_IA # lebih murah untuk data yang jarang diakses
# Step 4: hapus backup lokal yang lebih tua dari RETENTION_DAYS
find "$BACKUP_DIR" -name "*.sql.gz*" -mtime +$RETENTION_DAYS -delete
# Step 5: hapus backup S3 yang lebih tua dari 30 hari (lifecycle policy lebih baik)
# Atau konfigurasikan S3 Lifecycle Rules untuk otomasi ini
echo "Backup selesai: backup_${TIMESTAMP}.sql.gz.gpg"
Uji Restore: Satu-satunya Cara Membuktikan Backup Valid #
Backup yang tidak pernah direstore adalah asumsi, bukan jaminan. File backup bisa corrupt tanpa terdeteksi. Prosedur restore bisa sudah tidak akurat karena perubahan versi database. Estimasi waktu restore bisa salah. Semua ini hanya ketahuan ketika restore benar-benar dilakukan.
Jadwal uji restore yang disarankan:
Uji mingguan:
→ Restore satu tabel dari backup terbaru ke database staging
→ Verifikasi row count, spot-check beberapa baris
→ Estimasi waktu: 30 menit
Uji bulanan:
→ Full restore ke environment terpisah
→ Jalankan smoke test aplikasi terhadap database hasil restore
→ Verifikasi integrity: checksum, foreign key, data consistency
→ Estimasi waktu: 2–4 jam tergantung ukuran DB
Uji kuartalan (disaster recovery drill):
→ Simulasikan skenario bencana nyata
→ Hitung actual RTO dari awal sampai sistem online kembali
→ Libatkan engineer on-call untuk memvalidasi runbook
→ Update runbook berdasarkan temuan
# Contoh script uji restore otomatis (bisa dijalankan via CI/CD)
#!/bin/bash
BACKUP_FILE="$1" # path ke file backup yang ingin diuji
TEST_DB="restore_test_$(date +%Y%m%d)"
echo "Membuat database test: $TEST_DB"
mysql -u root -p"$ROOT_PASSWORD" -e "CREATE DATABASE $TEST_DB;"
echo "Restore dimulai: $(date)"
START_TIME=$(date +%s)
gunzip < "$BACKUP_FILE" | mysql -u root -p"$ROOT_PASSWORD" "$TEST_DB"
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo "Restore selesai dalam ${DURATION} detik"
echo "Verifikasi tabel:"
mysql -u root -p"$ROOT_PASSWORD" "$TEST_DB" -e "SHOW TABLES;"
echo "Verifikasi row count:"
mysql -u root -p"$ROOT_PASSWORD" "$TEST_DB" -e "
SELECT table_name, table_rows
FROM information_schema.tables
WHERE table_schema = '$TEST_DB'
ORDER BY table_rows DESC;"
echo "Cleanup database test:"
mysql -u root -p"$ROOT_PASSWORD" -e "DROP DATABASE $TEST_DB;"
echo "Uji restore selesai. Actual restore time: ${DURATION} detik"
Enkripsi dan Keamanan Backup #
Backup sering berisi data paling sensitif yang ada di sistem — seluruh database, termasuk password yang di-hash, nomor kartu kredit yang terenkripsi, data medis, dan informasi pribadi. Backup yang tidak dienkripsi yang jatuh ke tangan yang salah adalah bencana keamanan, bukan hanya masalah teknis.
Lapisan keamanan backup:
1. Enkripsi in transit
→ Gunakan HTTPS/TLS saat upload ke object storage
→ Jangan transfer backup via HTTP atau FTP
2. Enkripsi at rest
→ Enkripsi file backup sebelum disimpan
→ Gunakan AES-256 atau GPG
→ Kunci enkripsi disimpan terpisah dari backup
3. Access control
→ Hanya akun khusus backup yang punya akses buat backup
→ Akun tersebut hanya punya hak SELECT dan LOCK TABLES (tidak WRITE)
→ S3 bucket policy: hanya IP atau role tertentu yang bisa baca
4. Rotasi kunci enkripsi
→ Ganti kunci enkripsi secara berkala
→ Re-enkripsi backup lama jika kunci lama dikompromikan
5. Audit trail
→ Log siapa yang mengakses backup, kapan, dan dari mana
→ Alert jika ada akses backup yang tidak biasa
-- Buat user khusus untuk backup dengan hak minimal (MySQL)
CREATE USER 'backup_user'@'localhost' IDENTIFIED BY 'strong_password_here';
-- Hak yang dibutuhkan untuk mysqldump dengan --single-transaction
GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES ON *.* TO 'backup_user'@'localhost';
GRANT RELOAD ON *.* TO 'backup_user'@'localhost'; -- untuk FLUSH TABLES
GRANT PROCESS ON *.* TO 'backup_user'@'localhost'; -- untuk SHOW PROCESSLIST
-- Jangan berikan hak INSERT, UPDATE, DELETE, DROP, atau SUPER
FLUSH PRIVILEGES;
Backup dari Replica, Bukan Primary #
Menjalankan backup dari database primary langsung bisa menimbulkan beban IO yang signifikan — terutama untuk physical backup atau mysqldump tanpa --single-transaction yang membutuhkan lock. Solusi yang lebih baik adalah menjalankan backup dari replica yang didedikasikan untuk itu.
Arsitektur backup dari replica:
Primary DB ──────────────────────────────────→ Replica Backup
(melayani query) (khusus backup)
│
┌─────────────────────┘
│
┌─────────▼──────────┐
│ Backup job │
│ (mysqldump / │
│ xtrabackup) │
└─────────┬──────────┘
│
┌─────────▼──────────┐
│ Object Storage │
│ (S3 / GCS) │
└────────────────────┘
Keuntungan:
→ Primary tidak terbebani IO backup
→ Query pengguna tidak terdampak saat backup berjalan
→ Replica bisa di-pause replikasi dulu sebelum backup untuk konsistensi
# Backup dari replica MySQL dengan mysqldump
# Pastikan replica sudah sync sebelum backup
mysql -u root -p -e "SHOW SLAVE STATUS\G" | grep "Seconds_Behind_Master"
# Pastikan hasilnya 0 atau sangat kecil
# Backup dari replica
mysqldump \
--single-transaction \
--master-data=2 \ # sertakan posisi binlog untuk PITR
--routines --triggers \
-h replica-host \
-u backup_user -p"$BACKUP_PASSWORD" \
mydb | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz
Anti-Pattern yang Harus Dihindari #
# ✗ Anti-pattern 1: backup yang tidak pernah diuji restore
# Backup berjalan setiap malam selama 2 tahun
# Tidak ada yang pernah mencoba restore
# Saat dibutuhkan: file corrupt, versi tidak kompatibel
# → Tidak ada data yang bisa dipulihkan
# ✓ Solusi: jadwalkan uji restore otomatis minimal sebulan sekali
# Script restore ke staging + verifikasi row count + alert jika gagal
────────────────────────────────────────────────────────────────────────────────
# ✗ Anti-pattern 2: backup di server yang sama dengan database
# Server crash atau disk gagal → primary data hilang DAN backup hilang
# ✓ Solusi: backup selalu ke lokasi yang berbeda (object storage, server lain)
────────────────────────────────────────────────────────────────────────────────
# ✗ Anti-pattern 3: tidak mengaktifkan binary logging / WAL
# Hanya ada full backup harian
# Insiden terjadi pukul 23:55 → kehilangan hampir 24 jam data
# ✓ Solusi: aktifkan binlog/WAL untuk PITR, sehingga kehilangan data minimal
────────────────────────────────────────────────────────────────────────────────
# ✗ Anti-pattern 4: backup tidak dienkripsi di object storage
# Bucket S3 misconfigured → public accessible
# Seluruh dump database bisa didownload siapapun
# ✓ Solusi: enkripsi semua backup sebelum upload, gunakan bucket policy ketat
────────────────────────────────────────────────────────────────────────────────
# ✗ Anti-pattern 5: tidak tahu RTO dan RPO sistem
# "Kita punya backup kok" — tapi berapa lama restore butuh waktu?
# Database 500GB via mysqldump: restore mungkin 6–8 jam
# Jika SLA bisnis menuntut sistem online dalam 1 jam, ini tidak memenuhi RTO
# ✓ Solusi: definisikan RPO dan RTO, uji actual restore time, sesuaikan strategi
────────────────────────────────────────────────────────────────────────────────
# ✗ Anti-pattern 6: retensi backup tidak terdefinisi
# Backup menumpuk tanpa batas → storage penuh → backup baru gagal
# ✓ Solusi: retention policy yang jelas + monitoring storage usage backup
Checklist Review Backup and Restore #
STRATEGI BACKUP:
□ RPO sudah didefinisikan dan disetujui dengan stakeholder bisnis
□ RTO sudah didefinisikan dan diuji dengan actual restore time
□ Jenis backup sudah dipilih sesuai kebutuhan RPO/RTO dan ukuran data
□ Binary logging (MySQL) atau WAL archiving (PostgreSQL) sudah aktif
□ Strategi backup berlapis (full + incremental/differential) sudah ada
KEAMANAN BACKUP:
□ Semua backup dienkripsi sebelum disimpan (AES-256 atau GPG)
□ Kunci enkripsi disimpan terpisah dari backup
□ Transfer backup menggunakan HTTPS/TLS
□ User backup hanya punya hak minimal (SELECT, SHOW VIEW, LOCK TABLES)
□ Akses ke storage backup dibatasi dengan access policy
LOKASI DAN RETENSI:
□ Backup disimpan di lokasi berbeda dari server database (3-2-1 rule)
□ Minimal satu salinan di region atau availability zone berbeda
□ Retention policy sudah didefinisikan untuk setiap jenis backup
□ Storage backup dimonitor agar tidak penuh mendadak
UJI RESTORE:
□ Uji restore dilakukan setidaknya sebulan sekali
□ Actual restore time sudah diukur dan memenuhi RTO
□ Prosedur restore terdokumentasi step-by-step di runbook
□ Runbook pernah dipraktikkan oleh engineer yang on-call
□ Uji PITR pernah dilakukan minimal sekali untuk memvalidasi binlog/WAL
MONITORING:
□ Backup job dimonitor — alert jika gagal atau tidak berjalan
□ Ukuran backup dimonitor — perubahan drastis bisa tanda masalah
□ Retensi binlog/WAL dimonitor — tidak boleh lebih pendek dari interval full backup
□ Storage backup dimonitor — alert jika mendekati batas
Ringkasan #
- Backup yang tidak pernah direstore bukan backup — ia adalah asumsi yang belum diuji. Jadwalkan uji restore secara rutin dan otomatis, bukan hanya saat insiden sudah terjadi.
- Definisikan RPO dan RTO sebelum memilih strategi — RPO menentukan seberapa sering backup harus diambil, RTO menentukan seberapa cepat restore harus bisa dilakukan. Keduanya adalah keputusan bisnis, bukan keputusan teknis semata.
- Full backup saja tidak cukup untuk RPO kecil — aktifkan binary logging (MySQL) atau WAL archiving (PostgreSQL) untuk memungkinkan point-in-time recovery ke detik yang tepat sebelum insiden.
- 3-2-1 rule adalah standar minimum — 3 salinan data, di 2 media berbeda, dengan 1 salinan offsite. Backup di server yang sama dengan database tidak memenuhi kriteria ini.
- Enkripsi backup adalah wajib, bukan opsional — backup berisi seluruh data paling sensitif di sistem. File backup yang tidak terenkripsi adalah risiko keamanan setara dengan database yang tidak dilindungi.
- Jalankan backup dari replica, bukan primary — menghindari beban IO di primary yang bisa mempengaruhi query pengguna, terutama untuk physical backup yang bisa memakan waktu lama.
- Retensi binlog/WAL harus lebih panjang dari interval full backup — jika full backup dilakukan mingguan tapi binlog hanya disimpan 3 hari, PITR hanya bisa dilakukan untuk 3 hari terakhir, bukan seminggu.
- User backup harus hak minimal — akun yang digunakan untuk backup tidak perlu INSERT, UPDATE, atau DROP. Prinsip least privilege berlaku di sini.
- Ukur actual restore time secara berkala — estimasi “mungkin sekitar 2 jam” tidak cukup. Ukur waktu restore yang sebenarnya dan pastikan ia memenuhi RTO yang sudah disepakati.
- Dokumentasi runbook adalah bagian dari backup — prosedur restore yang hanya ada di kepala seseorang adalah risiko nyata. Saat insiden terjadi di tengah malam, runbook yang jelas menentukan perbedaan antara pemulihan yang cepat dan kekacauan.