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.

← Sebelumnya: Recreate   Berikutnya: Rollback Strategy →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact