Menjalankan python manage.py migrate bersamaan dengan rolling update pada Kubernetes atau AWS ECS sering memicu insiden downtime jika melibatkan breaking schema changes. Saat kolom di-drop, di-rename, atau ditambahkan dengan constraint NOT NULL tanpa default value, worker lama yang masih aktif seketika gagal mengeksekusi query. Artikel ini membedah postmortem insiden deploy Django, strategi deteksi berbasis observabilitas, eksekusi safe rollback, dan arsitektur migrasi zero-downtime.
Anatomi Insiden: Timeline dan Race Condition Rolling Update
Downtime terjadi ketika proses deployment memicu migrasi database destruktif sebelum pod versi lama selesai mengakhiri siklus hidupnya (graceful shutdown).
Kronologi Kegagalan
- 14:02:00 UTC: Pipeline CI/CD menjalankan release step:
python manage.py migrate. Migrasi mengeksekusiALTER TABLE accounts_profile DROP COLUMN legacy_status;. - 14:02:15 UTC: Deployment controller Kubernetes memulai rolling update dengan strategi
maxSurge: 25%danmaxUnavailable: 0%. Pod baru mulai dibuat. - 14:02:20 UTC: Pod versi lama (V1) masih menerima 80% traffic publik. Django ORM pada V1 mengompilasi model dengan
SELECT legacy_status, ... FROM accounts_profile. - 14:02:22 UTC: Database PostgreSQL melempar exception fatal. Lonjakan HTTP 500 melonjak di ingress controller.
- 14:02:40 UTC: Celery worker versi lama memproses task dari Redis dan mengalami retry exhaustion berulang kali karena kolom yang direferensikan sudah hilang.
Akar Masalah (Root Cause)
Terjadi race condition antara eksekusi migrasi skema fisik PostgreSQL dengan state model Django pada memori pod lama. Django ORM memetakan kolom secara eksplisit saat inisialisasi query. Ketika skema DB berubah di luar ekspektasi model V1 yang masih aktif melayani request, database PostgreSQL membatalkan transaksi.
Observabilitas: Diagnosa OperationalError dan Metrik Kritis
Saat breaking migration terjadi, gejala pertama tampak pada APM (Application Performance Monitoring) dan structured logging dengan pola pesan error yang seragam.
1. Pola Stack Trace APM
APM seperti Datadog, Sentry, atau New Relic akan menangkap spike eksepsi berikut:
django.db.utils.OperationalError: no such column: legacy_status
# Atau pada PostgreSQL:
psycopg2.errors.UndefinedColumn: column "legacy_status" of relation "accounts_profile" does not exist
LINE 1: SELECT "accounts_profile"."id", "accounts_profile"."legacy_s...Ciri khas insiden ini adalah tingkat error terkonsentrasi hanya pada pod V1. Pod V2 (yang baru live) berjalan tanpa error, menghasilkan grafik metrik split-error yang membingungkan jika metrik deployment tidak di-tag per image tag atau pod revision.
2. Diagnosa Kesiapan via Kubernetes Readiness Probe
Kesalahan umum adalah mengarahkan readinessProbe ke endpoint Django yang mengeksekusi query model terdampak (misalnya /health/ yang menjalankan User.objects.first()).
# Konfigurasi bermasalah jika model User terdampak migrasi
readinessProbe:
httpGet:
path: /health/ready/
port: 8000
initialDelaySeconds: 5
periodSeconds: 10Jika probe mengeksekusi query pada tabel yang skemanya sedang bermutasi, probe pod lama akan gagal serentak sebelum pod baru siap, memicu kondisi di mana ingress tidak memiliki target backend sama sekali (HTTP 502/503 Service Unavailable).
Prosedur Safe Rollback dan Penanganan Skema DB
Rollback aplikasi tanpa memperbaiki skema database yang rusak hanya akan memperpanjang waktu insiden. Ikuti urutan operasi berikut secara disiplin.
Fase 1: Bekukan Pipeline dan Amankan Komponen Async
- Aborting Rollout: Hentikan deployment Kubernetes seketika untuk mencegah terminasi pod lama jika pod baru belum melayani traffic penuh:
kubectl rollout pause deployment/django-app - Isolasi Worker: Celery worker akan memakan task dan gagal berulang kali. Hentikan konsumsi antrean tanpa mematikan worker seketika (graceful stop):
celery -A core_project control cancel_consumer default_queue
Fase 2: Pemulihan Kompatibilitas Database Skema
Jika kolom terlanjur di-drop, aplikasi versi lama tidak bisa melayani traffic sampai kolom tersebut dikembalikan, setidaknya secara struktur.
Peringatan: Jangan langsung menjalankan git revert pada kode aplikasi sebelum skema fisik database kembali kompatibel dengan kode versi lama dan versi baru.Jalankan query DDL perbaikan darurat langsung ke database untuk memulihkan kompatibilitas kode V1 tanpa validasi constraint ketat:
-- Buat kembali kolom sementara yang nullable agar query SELECT V1 tidak crash
ALTER TABLE accounts_profile ADD COLUMN IF NOT EXISTS legacy_status VARCHAR(50) DEFAULT 'inactive';Fase 3: Rollback Aplikasi
Setelah database kembali menyediakan definisi kolom yang dibutuhkan kode V1, lakukan rollback deployment ke revisi sebelumnya:
kubectl rollout undo deployment/django-app
kubectl rollout resume deployment/django-appPastikan pod V1 kembali stabil, metrik 5xx turun ke 0%, baru pulihkan konsumsi antrean Celery.
Pencegahan: Expand-Contract Pattern dan CI/CD Migration Linting
Untuk mencapai deployment zero-downtime, perubahan skema database harus selalu bersifat backward-compatible dan dilakukan melalui fase bertahap.
Implementasi Pola Expand-Contract
Jangan pernah me-rename atau men-drop kolom dalam satu rilis tunggal. Pisahkan ke dalam tiga siklus rilis:
- Rilis 1 (Expand): Buat kolom baru sebagai nullable atau dengan safe default. Jangan ubah kode lama yang membaca kolom lama.
- Rilis 2 (Contract Layer 1): Tulis data ke kedua kolom (dual-write via signal atau logic save), atau pindahkan pembacaan data ke kolom baru via
SeparateDatabaseAndState. - Rilis 3 (Contract Layer 2): Setelah kode V1 tidak ada lagi yang aktif di cluster, hapus kolom lama secara fisik dari database.
Menggunakan migrations.SeparateDatabaseAndState
Operasi SeparateDatabaseAndState memungkinkan sinkronisasi state Django ORM tanpa memodifikasi DDL database yang memicu locks atau breaking changes.
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0005_previous_migration'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
# Django menganggap field ini sudah dihapus dari ORM
migrations.RemoveField(
model_name='profile',
name='legacy_status',
),
],
database_operations=[
# Database fisik belum dimodifikasi, mencegah error SELECT query dari worker lama
],
),
]Integrasi django-migration-linter pada CI/CD
Gunakan tooling otomatis untuk mendeteksi operasi migrasi berbahaya seperti DROP COLUMN, penambahan kolom non-nullable tanpa default, atau pengubahan tipe data secara destruktif sebelum kode di-merge ke branch utama.
# Tambahkan ke step GitHub Actions / GitLab CI
- name: Run Django Migration Linter
run: |
python -m pip install django-migration-linter
python manage.py migration_linter --project-path . --exclude-apps admin authTool ini akan memblokir PR jika terdeteksi pembuatan operasi DDL yang tidak kompatibel dengan rolling update, memaksa pengembang menggunakan pola ekspansi bertahap.
Aturan Rilis Zero-Downtime
Deploy Django yang aman bergantung pada pemisahan yang jelas antara migrasi struktur data dan logika bisnis aplikasi. Ikuti prinsip dasar berikut:
- Semua penambahan kolom baru harus bersifat
null=Trueatau memiliki default database eksplisit. - Gunakan connection pooler (misalnya PgBouncer) dengan statement timeout terukur agar migrasi yang membutuhkan lock tidak memblokir antrean request utama.
- Gunakan healthcheck terpisah:
/health/liveness/untuk memeriksa responsivitas container, dan/health/readiness/tanpa membebani tabel bisnis yang sedang bermutasi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!