Database Migration Strategy #
Database migration adalah bagian paling berisiko dari setiap deployment. Salah melakukannya bisa berarti data corruption, downtime tak terduga, atau rollback yang mustahil. Di Kubernetes dengan rolling update, tantangannya lebih kompleks: dua versi aplikasi bisa berjalan bersamaan dan keduanya harus bisa membaca database yang sama. Artikel ini membahas pola yang memungkinkan migration dilakukan dengan aman, bahkan tanpa downtime.
Masalah Utama: Two Versions, One Database #
Saat rolling update, selama beberapa menit v1 dan v2 berjalan bersamaan. Keduanya mengakses database yang sama.
Skenario berbahaya: migration tidak backward-compatible
Database state sebelum migration:
┌─────────────────────────────────┐
│ users: id, user_name, email │
└─────────────────────────────────┘
Migration dijalankan: rename user_name → username
Database state setelah migration:
┌─────────────────────────────────┐
│ users: id, username, email │
└─────────────────────────────────┘
v1 Pod (masih berjalan): SELECT user_name FROM users → ERROR
v2 Pod (baru): SELECT username FROM users → OK
Rolling update menyebabkan error selama transisi!
Expand-Contract Pattern (Evolve Pattern) #
Solusi untuk migration zero-downtime adalah expand-contract — pisahkan migration menjadi beberapa langkah yang setiap langkahnya backward-compatible.
Contoh: rename kolom user_name → username
Langkah 1: EXPAND
Database: tambah kolom baru tanpa hapus yang lama
ALTER TABLE users ADD COLUMN username VARCHAR(100);
UPDATE users SET username = user_name;
Setelah ini: kedua kolom ada
Deploy v2-interim: baca dari username, tulis ke KEDUANYA
INSERT INTO users (user_name, username, email) VALUES (...)
┌─────────────────────────────────────────────┐
│ users: id, user_name, username, email │
└─────────────────────────────────────────────┘
v1: baca/tulis user_name ✓
v2-interim: baca username, tulis keduanya ✓
Langkah 2: VERIFY
Pastikan semua data di username sudah lengkap dan konsisten
Pastikan tidak ada Pod v1 yang masih berjalan
Langkah 3: CONTRACT
Deploy v2-final: hanya baca/tulis username
DROP COLUMN user_name;
┌─────────────────────────────────────────────┐
│ users: id, username, email │
└─────────────────────────────────────────────┘
Dengan pola ini, setiap langkah bisa di-deploy secara rolling update tanpa downtime.
Urutan Deployment yang Aman #
Urutan menjalankan migration relatif terhadap deployment aplikasi sangat penting:
Aturan dasar:
Migration yang MENAMBAH (additive) → jalankan SEBELUM deploy aplikasi baru
Migration yang MENGHAPUS (destructive) → jalankan SETELAH aplikasi lama tidak ada
Urutan yang benar:
1. Migration additive (tambah kolom, tambah tabel):
kubectl apply -f migration-job.yaml ← migration dulu
kubectl apply -f deployment-v2.yaml ← deploy aplikasi baru setelahnya
2. Migration destructive (hapus kolom, hapus tabel):
kubectl apply -f deployment-v2.yaml ← deploy aplikasi baru dulu
# Tunggu semua Pod v1 hilang
kubectl rollout status deployment/api
kubectl apply -f migration-job.yaml ← baru hapus kolom lama
Menjalankan Migration sebagai Kubernetes Job #
Migration sebaiknya dijalankan sebagai Kubernetes Job — bukan sebagai bagian dari startup container aplikasi.
# Migration Job
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration-v2-001
namespace: production
spec:
backoffLimit: 3 # retry 3 kali jika gagal
activeDeadlineSeconds: 600 # timeout: 10 menit
template:
spec:
restartPolicy: OnFailure
initContainers:
# Tunggu database ready sebelum migration
- name: wait-for-db
image: busybox:1.35
command:
- sh
- -c
- |
until nc -z postgres-service 5432; do
echo "Waiting for database..."
sleep 2
done
containers:
- name: migrate
image: my-api:v2 # image yang sama dengan aplikasi
command: ["python", "manage.py", "migrate"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: database-url
volumes: []
# Jalankan migration dan pantau
kubectl apply -f migration-job.yaml -n production
kubectl wait job/db-migration-v2-001 --for=condition=complete \
-n production --timeout=10m
# Lihat log migration
kubectl logs job/db-migration-v2-001 -n production
# Jika berhasil, baru deploy aplikasi
kubectl apply -f deployment-v2.yaml -n production
Tooling: Flyway dan Liquibase #
Menggunakan migration tool yang proper memastikan migration bisa di-track, di-rollback, dan di-reproduce:
# Menggunakan Flyway sebagai init container
spec:
initContainers:
- name: flyway-migrate
image: flyway/flyway:9.22
args:
- -url=jdbc:postgresql://postgres-service:5432/appdb
- -schemas=public
- -user=$(DB_USER)
- -password=$(DB_PASS)
- migrate
env:
- name: DB_USER
valueFrom:
secretKeyRef:
name: db-credentials
key: username
- name: DB_PASS
valueFrom:
secretKeyRef:
name: db-credentials
key: password
volumeMounts:
- name: migrations
mountPath: /flyway/sql
volumes:
- name: migrations
configMap:
name: db-migrations # migration SQL files di ConfigMap
Struktur migration SQL dengan Flyway:
V001__create_users_table.sql
V002__add_email_index.sql
V003__add_username_column.sql ← additive
V004__remove_user_name_column.sql ← destructive (deploy setelah v3 stabil)
Skenario yang Memerlukan Maintenance Window #
Tidak semua migration bisa dilakukan dengan zero-downtime. Kapan maintenance window tidak terhindarkan:
Butuh maintenance window jika:
1. Perubahan tipe data yang tidak bisa dilakukan secara bertahap
ALTER COLUMN amount TYPE BIGINT (dari INTEGER)
→ Di PostgreSQL, operasi ini mengunci tabel
→ Untuk tabel besar: butuh pg_repack atau pg_squeeze dulu
2. Restrukturisasi major yang terlalu kompleks untuk expand-contract
→ Normalisasi denormalized data
→ Split tabel menjadi beberapa tabel
3. Database engine upgrade
→ Major version upgrade PostgreSQL/MySQL
→ Perlu window untuk backup, upgrade, verify
4. Perubahan yang memerlukan rebuild index besar
→ CREATE INDEX CONCURRENTLY sudah membantu, tapi ada kasus
di mana non-concurrent rebuild diperlukan
Ringkasan #
- Expand-contract untuk zero-downtime migration — pisahkan migration menjadi: tambah dulu (expand), deploy aplikasi baru yang kompatibel dengan keduanya, baru hapus yang lama (contract).
- Migration additive sebelum deploy, destructive setelah deploy — kolom baru harus ada sebelum aplikasi baru berjalan; kolom lama baru bisa dihapus setelah aplikasi lama tidak ada.
- Job Kubernetes untuk migration, bukan startup aplikasi — migration yang gagal tidak boleh menghalangi Pod aplikasi restart; Job punya lifecycle tersendiri dan bisa di-monitor.
- Migration tool (Flyway, Liquibase) untuk tracking — versi migration ter-track di database; rollback dan re-run bisa dilakukan dengan aman; tidak ada ambiguitas “sudah jalan atau belum”.
- Beberapa migration membutuhkan maintenance window — perubahan tipe data yang mengunci tabel, atau perubahan fundamental yang terlalu kompleks untuk expand-contract.
- Selalu backup sebelum migration — terutama untuk migration destructive; backup adalah jaring pengaman terakhir jika ada yang tidak terduga.