Database Migration Strategy #

Dalam arsitektur aplikasi monolitik tradisional, migrasi database (seperti mengubah tipe kolom, menambah tabel, atau mengubah relasi data) biasanya dilakukan saat jendela pemeliharaan (maintenance window) yang telah disepakati. Seluruh aplikasi akan dimatikan (downtime), skrip migrasi database dijalankan, dan aplikasi baru dinyalakan dari kondisi bersih. Namun, pada era cloud-native yang menuntut ketersediaan layanan 24/7, pendekatan ini tidak lagi relevan. Kita harus mampu melakukan migrasi database tanpa downtime sedikit pun.

Di Kubernetes, tantangan migrasi database menjadi berlipat ganda karena mekanisme Rolling Update. Selama masa transisi rilis yang berlangsung beberapa menit, Pod versi lama (v1) dan Pod versi baru (v2) akan berjalan secara bersamaan di dalam cluster dan keduanya mengakses database fisik yang sama. Jika kita langsung menerapkan perubahan skema database yang memotong kompatibilitas mundur (non-backward compatible), salah satu versi Pod dipastikan akan mengalami kegagalan kueri massal. Artikel ini mengupas strategi orkestrasi skema database menggunakan pola Expand-Contract, implementasi menggunakan Kubernetes Jobs, serta integrasi perkakas migrasi otomatis (migration tooling) di produksi.


Masalah Klasik: Satu Database untuk Dua Versi Aplikasi #

Ketika kita memicu Rolling Update di Kubernetes, kita secara tidak langsung menciptakan masa transisi di mana dua versi logika aplikasi yang berbeda berinteraksi dengan satu database secara simultan.

Misalkan kita ingin mengubah nama kolom tabel database dari user_name menjadi username:

flowchart TD
    Traffic["Trafik Pengguna"] --> PodV1["Pod v1 (Lama)"]
    Traffic --> PodV2["Pod v2 (Baru)"]

    PodV1 -->|"SELECT user_name"| DB["Database Bersama (Shared DB)\n(Struktur tabel: id, username, email)"]
    PodV2 -->|"SELECT username"| DB

Jika skrip migrasi database langsung mengubah nama kolom sebelum Rolling Update dimulai:

  • Pod v2 (baru) yang sedang startup akan berjalan lancar karena kueri SELECT username menemukan kolomnya di database.
  • Pod v1 (lama) yang masih aktif melayani sisa request pengguna akan langsung menghasilkan error HTTP 500 karena kueri SELECT user_name gagal mengeksekusi kolom yang sudah tidak ada.

Tujuan utama dari strategi migrasi database di Kubernetes adalah menjamin kompatibilitas mundur (backward-compatibility) di setiap langkah transisi, sehingga tidak ada satu pun kueri dari versi v1 maupun v2 yang mengalami kegagalan.


Pola Expand-Contract (Evolve Pattern) #

Untuk memecahkan masalah di atas secara elegan tanpa downtime, kita menggunakan pola Expand-Contract (terkadang disebut Parallel Run). Pola ini memecah satu perubahan skema database yang melanggar kompatibilitas mundur menjadi lima fase rilis terpisah yang masing-masing fasenya aman bagi semua versi aplikasi yang aktif.

Mari kita ambil contoh kasus mengubah nama kolom user_name menjadi username pada tabel users:

flowchart TD
    Start["Fase 1: State Awal<br/>'(Hanya ada kolom user_name)'"] --> Step1["Fase 2: Expand (Additive)<br/>'(Buat kolom username, sinkronisasi data)'"]
    Step1 --> Step2["Fase 3: Deploy Interim App<br/>'(Aplikasi membaca user_name, menulis ke KEDUANYA)'"]
    Step2 --> Step3["Fase 4: Backfill & Verify<br/>'(Migrasi baris lama, alihkan baca ke username)'"]
    Step3 --> Step4["Fase 5: Contract (Destructive)<br/>'(Hapus kolom lama user_name dari database)'"]
    
    style Step1 stroke:#0288d1,stroke-width:2px
    style Step3 stroke:#f57c00,stroke-width:2px
    style Step4 stroke:#388e3c,stroke-width:2px

Fase 1: Kondisi Awal #

Skema database awal hanya memuat kolom lama:

  • Aplikasi v1 membaca dan menulis ke kolom user_name.

Fase 2: Tahap Expand (Additive Database Release) #

Kita menambahkan kolom baru tanpa menyentuh atau menghapus kolom lama.

  • Database Schema: Jalankan kueri SQL untuk membuat kolom baru username (diizinkan bernilai Null sementara waktu).
    ALTER TABLE users ADD COLUMN username VARCHAR(255) NULL;
    
  • Aplikasi: Pod v1 tetap berjalan dan hanya berinteraksi dengan user_name.

Fase 3: Tahap Deploy Interim Application (Dual-Write) #

Kita merilis kode aplikasi versi perantara (interim/v1.5) ke dalam cluster menggunakan Rolling Update.

  • Logika Aplikasi: Aplikasi interim ini dirancang khusus untuk membaca dari kolom lama (user_name), tetapi menulis ke kedua kolom sekaligus (user_name dan username) untuk setiap transaksi baru (dual-write).
  • Hasil: Baris data baru dipastikan memiliki nilai yang sinkron di kedua kolom. Pod v1 (lama) yang masih tersisa tetap aman membaca user_name.

Fase 4: Tahap Data Backfill & Validasi #

Data historis sebelum fase 3 belum memiliki nilai di kolom username yang baru. Kita harus menjalankan skrip batch (data backfill) secara perlahan di balik layar untuk menyalin nilai dari user_name ke username pada baris-baris lama yang belum terupdate.

-- Dijalankan secara bertahap (batching) untuk menghindari table lock pada database besar
UPDATE users SET username = user_name WHERE username IS NULL;

Setelah seluruh data sinkron 100%, kita merilis aplikasi versi v2.

  • Logika Aplikasi v2: Membaca dan menulis secara eksklusif hanya ke kolom baru (username).
  • Rolling Update: Seluruh Pod interim (v1.5) digantikan oleh Pod v2. Setelah transisi selesai, tidak ada lagi proses yang membaca kolom user_name.

Fase 5: Tahap Contract (Destructive Database Release) #

Fase terakhir adalah pembersihan. Karena tidak ada lagi aplikasi di cluster yang mengakses kolom lama, kita dapat menghapus kolom user_name secara aman tanpa risiko crash.

ALTER TABLE users DROP COLUMN user_name;

Urutan Deployment dan Orkestrasi Siklus Hidup #

Urutan pelaksanaan migrasi database relatif terhadap waktu deployment aplikasi di Kubernetes adalah aturan besi yang tidak boleh dilanggar:

  • Pernyataan Tambahan (Additive Migrations): Seperti membuat tabel baru, menambahkan kolom baru, atau membuat indeks baru.
    • Aturan: Wajib dijalankan SEBELUM aplikasi versi baru (v2) dideploy. Jika kontainer v2 menyala dan tidak menemukan kolom baru yang dibutuhkannya, aplikasi langsung mengalami crash loop.
  • Pernyataan Penghapusan (Destructive Migrations): Seperti menghapus kolom lama, menjatuhkan indeks usang, atau menghapus tabel.
    • Aturan: Wajib dijalankan SETELAH seluruh Pod aplikasi versi lama (v1) mati sepenuhnya dari cluster.

Menjalankan Migrasi Menggunakan Kubernetes Jobs #

Salah satu anti-pattern yang paling umum adalah menyisipkan perintah migrasi database ke dalam skrip inisialisasi kontainer aplikasi (misalnya pada Docker ENTRYPOINT atau CMD seperti python manage.py migrate atau npm run db:migrate).

Mengapa In-Container Migration Sangat Berbahaya? #

  1. Kondisi Balapan (Race Conditions): Jika kita melakukan scaling Deployment menjadi 5 replika Pod, kelima Pod baru tersebut akan mencoba mengeksekusi skrip migrasi database yang sama secara simultan saat startup. Hal ini memicu deadlock pada tabel metadata migrasi database.
  2. Overhead Kinerja & Timeout: Inisialisasi migrasi yang memakan waktu lama dapat menyebabkan startup/readiness probe aplikasi gagal, memaksa Kubernetes menghentikan kontainer di tengah proses migrasi yang belum selesai.
  3. Masalah Hak Akses (Least Privilege): Pod aplikasi kita terpaksa harus memegang kredensial database tingkat admin (yang memiliki izin ALTER, DROP, CREATE) selama masa hidupnya, padahal saat runtime biasa aplikasi hanya membutuhkan hak akses manipulasi data biasa (SELECT, INSERT, UPDATE).

Solusi Terbaik: Kubernetes Jobs #

Gunakan objek Job Kubernetes terpisah untuk mengeksekusi migrasi database. Job dirancang untuk menjalankan satu tugas hingga selesai (run-to-completion) dan hanya berjalan sekali (single pod execution).

sequenceDiagram
    participant CI as Pipeline CI/CD
    participant Job as K8s Migration Job
    participant DB as Database Server
    participant Deploy as K8s Deployment
    
    CI->>Job: Terapkan Manifes Job Migrasi
    Job->>DB: Jalankan Skrip SQL Additive (Expand)
    DB-->>Job: Migrasi Berhasil (Exit Code 0)
    Job-->>CI: Job Status: Complete
    CI->>Deploy: Terapkan Update Deployment (Rolling Update v2)
    Note over Deploy: Pod baru berjalan lancar karena skema database siap!

Manifes Kubernetes Job untuk Migrasi Database (Menggunakan Flyway) #

Berikut adalah contoh manifes Job siap produksi yang menggunakan tools migrasi populer Flyway untuk mengorkestrasi perubahan skema database secara aman:

apiVersion: batch/v1
kind: Job
metadata:
  name: database-migration-v1-11-0
  namespace: backend
spec:
  # Batas toleransi percobaan ulang jika Job gagal (3 kali)
  backoffLimit: 3
  # Batas waktu timeout total eksekusi Job (10 menit)
  activeDeadlineSeconds: 600
  template:
    metadata:
      labels:
        app: database-migration
    spec:
      # Job dihentikan jika sukses, coba restart kontainer jika gagal
      restartPolicy: OnFailure
      initContainers:
      # initContainer khusus untuk memverifikasi database siap menerima koneksi (TCP check)
      - name: wait-for-postgres
        image: busybox:1.36
        command:
        - sh
        - -c
        - |
          until nc -z postgres-service.database.svc.cluster.local 5432; do
            echo "Waiting for PostgreSQL database connection..."
            sleep 2
          done          
      containers:
      - name: flyway
        image: flyway/flyway:9.22-alpine
        args:
        - -url=jdbc:postgresql://postgres-service.database.svc.cluster.local:5432/app_db
        - -schemas=public
        - -user=$(DB_USER)
        - -password=$(DB_PASSWORD)
        - -connectRetries=10
        - migrate
        env:
        - name: DB_USER
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: admin-username
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: admin-password
        volumeMounts:
        - name: sql-migrations
          mountPath: /flyway/sql
      volumes:
      # Menyimpan file kueri SQL migrasi di dalam ConfigMap
      - name: sql-migrations
        configMap:
          name: db-sql-migrations-v1-11-0

Skenario Kritis yang Membutuhkan Maintenance Window #

Meskipun pola Expand-Contract dapat menyelesaikan sebagian besar masalah migrasi, ada skenario-skenario infrastruktur tertentu di mana waktu henti layanan (maintenance window / downtime) tetap tidak terhindarkan demi menghindari degradasi database total.

1. Perubahan Tipe Data Kolom pada Tabel Raksasa #

Mengubah tipe data kolom (misalnya dari INTEGER ke BIGINT untuk kolom ID transaksi yang mulai habis kapasitasnya) pada tabel yang memuat ratusan juta baris data.

  • Masalah: Perintah ALTER TABLE ... ALTER COLUMN di sebagian besar mesin database relational (seperti PostgreSQL atau MySQL) akan mengunci tabel secara eksklusif (Exclusive Table Lock). Operasi ini memaksa database menulis ulang seluruh data di disk host, yang membutuhkan waktu puluhan menit hingga hitungan jam. Selama durasi tersebut, seluruh kueri tulis aplikasi akan stuck (antrean transaksi menumpuk) dan berujung pada kegagalan layanan total.
  • Mitigasi Tanpa Downtime: Membutuhkan taktik khusus seperti penggunaan tools pihak ketiga (e.g. pg_repack di PostgreSQL atau gh-ost/pt-online-schema-change di MySQL) yang membuat tabel bayangan (shadow table) dan memigrasikan data secara asinkron di balik layar. Jika tools ini tidak tersedia, maintenance window adalah satu-satunya pilihan aman.

2. Migrasi Mesin Database (Major Engine Upgrade) #

Melakukan upgrade versi major mesin database (misalnya upgrade PostgreSQL dari versi 12 ke 15) atau memindahkan database fisik dari infrastruktur lokal (on-premise) ke layanan managed database cloud (seperti AWS RDS atau GCP Cloud SQL).

  • Masalah: Proses ekspor-impor data (dump & restore) membutuhkan jeda waktu di mana data di database asal tidak boleh mengalami perubahan sama sekali untuk menghindari inkonsistensi data akhir (data drift).
  • Solusi: Terapkan jendela pemeliharaan, aktifkan halaman pemeliharaan pada Ingress, lakukan backup penuh, jalankan replikasi data CDC (Change Data Capture) jika memungkinkan untuk meminimalkan window, dan lakukan cutover secara terencana.

Anti-Pattern vs Solusi Terbaik #

Berikut adalah kompilasi kesalahan umum dalam manajemen migrasi database di Kubernetes beserta perbaikannya.

Anti-Pattern 1: Mengabaikan Penyusunan Skrip Rollback Database yang Aman #

Tim developer hanya fokus menulis skrip migrasi naik (Up Migration), tetapi tidak menyiapkan atau menguji skrip migrasi turun (Down/Rollback Migration). Ketika aplikasi versi baru (v2) dideploy dan memicu error kritis di produksi, operator segera menjalankan perintah rollback aplikasi (kubectl rollout undo). Namun, karena skema database terlanjur berubah dan tidak di-rollback, Pod v1 yang baru saja aktif kembali langsung mengalami crash loop karena tidak dapat membaca format kolom baru.

Solusi Terbaik #

Setiap kali menulis skrip migrasi database (misalnya file SQL V1.2.0__add_discount.sql), wajib buat skrip rollback padanannya (misalnya U1.2.0__add_discount.sql di Flyway). Skrip rollback ini harus diuji coba secara berkala di lingkungan staging untuk menjamin keandalannya jika terjadi kondisi darurat di produksi.


Anti-Pattern 2: Menghapus Kolom Lama di Fase Awal (Expand) Secara Bersamaan #

Menggabungkan kueri penambahan kolom baru dan kueri penghapusan kolom lama di dalam satu file skrip migrasi SQL yang sama demi kepraktisan. Hal ini membatalkan prinsip backward-compatibility secara instan selama proses Rolling Update.

Solusi Terbaik #

Pecah proses menjadi dua rilis terpisah. Rilis pertama hanya bersifat Additive (menambah). Rilis kedua yang bersifat Destructive (menghapus) hanya boleh dieksekusi setelah aplikasi baru terbukti stabil di produksi selama minimal beberapa hari.


Checklist Audit Migrasi Database Produksi #

Pastikan tim pengembang dan operator melakukan audit menggunakan checklist berikut sebelum mengeksekusi migrasi database di lingkungan produksi:

KOMPATIBILITAS SKEMA & DATA:
  □ Skema database baru telah terverifikasi bersifat backward-compatible dengan kode Pod lama (v1).
  □ Pola Expand-Contract telah diterapkan untuk breaking changes (misal ganti nama kolom / tipe data).
  □ Skrip data backfill disiapkan secara terpisah dengan batasan ukuran batch untuk menghindari CPU spike database.
  □ Skrip rollback database (Down Migration) telah dibuat dan diuji kelayakannya.

ORKESTRASI KUBERNETES JOBS:
  □ Skrip migrasi dijalankan melalui objek Kubernetes Job terpisah (bukan in-app startup).
  □ Job migrasi dilengkapi initContainer 'wait-for-db' untuk memverifikasi koneksi database.
  □ Kredensial database untuk Job migrasi menggunakan akun khusus dengan izin admin, terpisah dari runtime Pod.
  □ Batas waktu timeout (activeDeadlineSeconds) Job migrasi telah diatur secara wajar.

PROSEDUR EMERGENCY & BACKUP:
  □ Backup database otomatis (snapshot) dipastikan telah berjalan dengan sukses sebelum Job migrasi dimulai.
  □ Prosedur pemulihan data (restore backup) telah diuji coba untuk mengukur RTO (Recovery Time Objective).
  □ Tim operator memiliki akses cepat ke log eksekusi Job migrasi (`kubectl logs job/...`) untuk memantau kesalahan sintaksis SQL.

Ringkasan #

  • Expand-Contract untuk zero-downtime — Pecah migrasi database yang melanggar kompatibilitas mundur menjadi beberapa fase: tambah kolom (expand), deploy aplikasi interim dual-write, backfill data, dan terakhir hapus kolom usang (contract).
  • Gunakan Kubernetes Jobs — Jangan pernah menjalankan migrasi database otomatis di dalam thread utama startup kontainer aplikasi untuk menghindari deadlock dan pembatasan hak akses runtime.
  • Additive sebelum, Destructive sesudah — Jalankan kueri SQL penambahan kolom sebelum deployment Pod baru dimulai, dan jalankan kueri penghapusan setelah seluruh Pod lama mati dari cluster.
  • Gunakan initContainers wait-for-db — Lengkapi Job migrasi dengan initContainer yang melakukan pengecekan koneksi TCP port database sebelum kontainer migrasi utama dijalankan.
  • Pahami limitasi table lock — Operasi restrukturisasi tabel raksasa (seperti ganti tipe data) memicu lock eksklusif yang memerlukan maintenance window jika tidak menggunakan tools dynamic schema migration.
  • Wajib uji skrip rollback — Selalu siapkan skrip rollback database (down migration) yang selaras dengan revisi aplikasi untuk menjamin kelancaran penanganan kondisi darurat.

← Sebelumnya: Recreate   Berikutnya: Rollback Strategy →

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