Job & CronJob #

Di dalam kluster Kubernetes, tidak semua beban kerja (workloads) didesain untuk menyala terus-menerus tanpa henti. Jika aplikasi web API, gateway, dan database harus siaga melayani trafik setiap detiknya, ada jenis tugas lain yang sifatnya transien—yakni berjalan untuk memproses sesuatu, selesai dengan sukses, lalu mati secara bersih. Contohnya adalah melakukan migrasi database, menghasilkan laporan keuangan bulanan, mengirimkan email promosi massal, atau memproses antrean berkas video. Untuk menangani tugas sekali selesai (run-to-completion) ini, Kubernetes menyediakan objek Job dan CronJob.

Menggunakan Deployment untuk tugas transien adalah kesalahan fatal karena Deployment akan terus mencoba menghidupkan kembali kontainer yang sudah sukses berjalan. Memahami arsitektur internal Job dan CronJob, parameter pemrosesan paralel, serta kebijakan penanganan kegagalan (failure handling) sangatlah penting agar kita dapat mengotomatisasi pemrosesan batch skala besar secara efisien di kluster produksi.


Batch Workloads: Filosofi Run-to-Completion #

Perbedaan fundamental antara layanan tanpa henti (long-running services) dengan tugas sekali selesai (batch processing) terletak pada penafsiran kode keluar (exit code) kontainer:

  • Deployment: Menganggap keluarnya kontainer aplikasi (baik dengan exit code 0 maupun bukan) sebagai anomali. Deployment Controller akan selalu memerintahkan Kubelet untuk merestart kontainer demi menjaga ketersediaan layanan.
  • Job: Memiliki pemahaman yang berbeda. Jika kontainer utama di dalam Pod keluar dengan kode exit 0 (sukses), Job Controller akan menghentikan seluruh daur hidup Pod tersebut, menandainya sebagai Completed (Selesai), dan tidak mencoba merestartnya kembali.

Berikut adalah visualisasi alur proses transisi status Job dari awal pembuatan hingga pembersihan otomatis oleh Kubelet:

flowchart TD
    Created["Job Dibuat"] --> Schedule["Pod Dijadwalkan"]
    Schedule --> Running["Pod Berjalan"]
    
    Running --> ExitCheck{"Apakah Kontainer\nExit Code = 0?"}
    ExitCheck -- "Ya" --> Succeeded["Job Selesai Sukses (Succeeded)"]
    ExitCheck -- "Tidak" --> RestartPolicyCheck{"restartPolicy?"}
    
    RestartPolicyCheck -- "OnFailure" --> RestartContainer["Restart Kontainer di Pod yang Sama"]
    RestartContainer --> Running
    
    RestartPolicyCheck -- "Never" --> NewPod["Buat Pod Baru di Node Berbeda"]
    NewPod --> BackoffCheck{"backoffLimit\nTercapai?"}
    
    BackoffCheck -- "Belum" --> Running
    BackoffCheck -- "Ya" --> Failed["Job Dinyatakan Gagal (Failed)"]
    
    Succeeded --> TTL{"ttlSecondsAfterFinished\nTercapai?"}
    Failed --> TTL
    
    TTL --> Delete["Hapus Job & Pod Otomatis dari etcd"]

Objek Job: Penanganan Tugas Sekali Selesai #

Objek Job memastikan bahwa satu atau beberapa Pod berhasil menyelesaikan tugasnya secara tuntas.

Berikut adalah contoh manifest Job tingkat produksi untuk melakukan migrasi basis data secara aman:

apiVersion: batch/v1
kind: Job
metadata:
  name: database-migrator
  namespace: production
spec:
  completions: 1                  # Jumlah keberhasilan sukses yang wajib dicapai
  parallelism: 1                  # Jumlah Pod yang boleh berjalan secara bersamaan
  backoffLimit: 4                 # Toleransi maksimum kegagalan sebelum Job menyerah
  activeDeadlineSeconds: 600      # Batas waktu maksimal eksekusi Job (10 menit)
  template:
    spec:
      restartPolicy: OnFailure    # OnFailure | Never (DILARANG keras memakai Always)
      containers:
      - name: migrator-app
        image: company-app:v3.2.0
        command: ["/app/bin/migrate", "up"]
        resources:
          requests:
            cpu: "500m"
            memory: "512Mi"
          limits:
            cpu: "1000m"
            memory: "1Gi"

Memilih restartPolicy: Never vs OnFailure #

Di dalam manifest Job, kita dilarang keras menuliskan restartPolicy: Always. Kita wajib memilih salah satu di antara dua opsi kebijakan berikut:

  1. OnFailure: Jika kontainer di dalam Pod keluar dengan error (exit code bukan 0), Kubelet akan me-restart kontainer tersebut di dalam Worker Node dan Pod yang sama. Pola ini sangat efisien karena menghindari overhead penjadwalan ulang Pod baru.
  2. Never: Jika kontainer mengalami kegagalan, Kubelet tidak akan merestartnya. Sebaliknya, Job Controller akan meminta Scheduler untuk menciptakan Pod baru di Worker Node lain guna melanjutkan tugas yang gagal. Pola ini sangat tepat jika kegagalan disebabkan oleh kerusakan sistem fisik node host.

Kebijakan Penanganan Kegagalan Tingkat Lanjut (podFailurePolicy) #

Pada versi Kubernetes modern (v1.25+), kita dapat menggunakan fitur podFailurePolicy untuk menangani kegagalan eksekusi secara jauh lebih presisi dibandingkan hanya mengandalkan backoffLimit global. Tanpa kebijakan ini, setiap kegagalan Pod apa pun penyebabnya (apakah itu bug kode aplikasi, kehabisan memori (OOM), atau kegagalan infrastruktur) akan dihitung sama dan mengurangi jatah backoffLimit.

Dengan podFailurePolicy, kita dapat membedakan tindakan berdasarkan Exit Code kontainer atau Kondisi Kegagalan Pod:

spec:
  backoffLimit: 6
  podFailurePolicy:
    rules:
    - action: FailJob             # Gagalkan Job SEGERA jika exit code adalah 42 (misal: Error Fatal)
      onExitCodes:
        containerName: migrator-app
        operator: In
        values: [42]
    - action: Ignore              # Jangan hitung kegagalan ini ke dalam backoffLimit jika Pod di-evict (misal karena node maintenance)
      onPodConditions:
      - type: DisruptionTarget    # Mengindikasikan Pod dihentikan karena gangguan eksternal

Keuntungan Implementasi podFailurePolicy: #

  • Menghemat Resource & Waktu: Jika aplikasi mendeteksi kesalahan konfigurasi atau kesalahan kredensial database (yang menghasilkan exit code spesifik, misal 42), tidak ada gunanya Kubernetes mencoba menjalankan ulang Job 6 kali lagi. Job akan langsung ditandai gagal seketika, mempercepat feedback loop CI/CD.
  • Ketahanan Terhadap Gangguan Infrastruktur: Jika Pod mati karena node mengalami spot instance eviction or maintenance scaling, kita tidak ingin hal itu dianggap sebagai kesalahan aplikasi. Dengan aturan Ignore, Job tetap akan dijalankan kembali tanpa mengurangi kuota percobaan gagal kita.

Pola Desain Kombinasi Completions dan Parallelism #

Kita dapat merancang berbagai variasi pola eksekusi pemrosesan data batch dengan mengombinasikan nilai properti completions (target sukses) dan parallelism (kapasitas konkuren):

  • Tugas Tunggal (Single Job): completions: 1 dan parallelism: 1. Menjalankan satu Pod tunggal hingga selesai. Tepat untuk inisialisasi skema DB.
  • Urutan Berurutan (Sequential Batch): completions: 5 dan parallelism: 1. Menjalankan 5 tugas secara bergantian satu per satu. Pod ke-2 baru akan dibuat setelah Pod ke-1 selesai sukses.
  • Paralel Skala Besar (Parallel Batch): completions: 100 dan parallelism: 10. Menjalankan 10 Pod secara konkuren bersamaan hingga mencapai total 100 sukses eksekusi.
  • Antrean Kerja (Work Queue): completions tidak diset (dibiarkan kosong) dan parallelism: 5. Menjalankan 5 Pod secara konkuren. Setiap Pod akan mengambil data secara dinamis dari server antrean eksternal (seperti RabbitMQ). Job dinyatakan selesai begitu antrean kosong dan seluruh kontainer keluar dengan sukses.

Penggunaan Indeks Penjadwalan Modern (JOB_COMPLETION_INDEX) #

Sejak Kubernetes versi 1.21+, jika kita menggunakan jenis Indexed Job (completionMode: Indexed), Kubernetes secara otomatis akan menyuntikkan nomor indeks unik ke dalam setiap Pod di bawah anotasi batch.kubernetes.io/job-completion-index (dari 0 s.d. completions-1).

Aplikasi kita dapat membaca indeks ini melalui Downward API untuk membagi rentang data pemrosesan secara merata:

# Pod indeks 0 memproses data ID 1-1000, Pod indeks 1 memproses ID 1001-2000, dst.
env:
- name: MY_WORK_INDEX
  valueFrom:
    fieldRef:
      fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']

Kebijakan Pembersihan Otomatis (TTL Controller) #

Secara default, setelah sebuah Job selesai berjalan sukses (Succeeded) atau gagal (Failed), objek Job beserta Pod turunannya tetap dipertahankan di database etcd kluster dengan status Completed atau Error. Hal ini sengaja dilakukan agar administrator dapat membaca log keluaran atau mendeskripsikan kegagalannya.

Namun, di lingkungan CI/CD padat yang menjalankan ratusan Job setiap hari, membiarkan ribuan Pod Completed menumpuk akan mengotori tampilan CLI dan membebani performa cache Kubelet di node host.

Untuk mengotomatisasi pembersihan, kita wajib menggunakan properti ttlSecondsAfterFinished:

spec:
  ttlSecondsAfterFinished: 3600 # Hapus Job beserta seluruh PodCompleted secara otomatis 1 jam setelah selesai

Objek CronJob: Otomatisasi Tugas Terjadwal #

CronJob digunakan untuk membungkus objek Job agar dapat dijalankan secara berkala menggunakan sintaks jadwal Linux Cron standar.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-db-cleanup
  namespace: production
spec:
  schedule: "0 2 * * *"                 # Dijalankan setiap hari jam 02:00 pagi
  timeZone: "Asia/Jakarta"              # Menetapkan zona waktu lokal kluster (K8s 1.25+)
  concurrencyPolicy: Forbid             # Mengontrol aturan tumpang tindih eksekusi
  startingDeadlineSeconds: 180          # Batas toleransi keterlambatan start (3 menit)
  successfulJobsHistoryLimit: 3         # Simpan log 3 Job sukses terakhir
  failedJobsHistoryLimit: 1             # Simpan log 1 Job gagal terakhir
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: clean-agent
            image: db-utils:v1.0
            command: ["/app/clean-expired-sessions"]

Kebijakan Concurrency (Concurrency Policy) pada CronJob #

Salah satu masalah kritis pada tugas terjadwal adalah ketika sebuah eksekusi Job berjalan sangat lambat, sehingga waktu jadwal berikutnya tiba sebelum Job lama selesai. Kita wajib mengendalikan perilaku ini menggunakan concurrencyPolicy:

Tiga Opsi Kebijakan Concurrency: #

  1. Allow (Default): Mengizinkan beberapa Job berjalan tumpang tindih secara bersamaan. Ini berbahaya jika tugas tersebut memodifikasi database yang sama secara konkuren.
  2. Forbid: Jika Job sebelumnya belum selesai, jadwal baru akan dilewati (skipped). Ini adalah pilihan paling aman untuk tugas pembersihan atau backup database.
  3. Replace: Jika Job lama masih berjalan, Kubernetes akan langsung mematikan (SIGKILL) Job lama tersebut dan menggantikannya dengan Job baru yang baru saja dipicu.

Berikut visualisasi perbedaan perilaku antara kebijakan Forbid dan Replace:

flowchart TD
    subgraph ForbidPolicy ["ConcurrencyPolicy: Forbid"]
        F_Start["Waktu Jadwal 2 Tiba"] --> F_Check{"Apakah Job 1\nmasih berjalan?"}
        F_Check -- "Ya" --> F_Skip["Lewati / Batalkan Jadwal 2\n(Job 1 dibiarkan selesai)"]
        F_Check -- "Tidak" --> F_Run["Jalankan Job 2"]
    end

    subgraph ReplacePolicy ["ConcurrencyPolicy: Replace"]
        R_Start["Waktu Jadwal 2 Tiba"] --> R_Check{"Apakah Job 1\nmasih berjalan?"}
        R_Check -- "Ya" --> R_Kill["Kirim SIGKILL untuk matikan Job 1"]
        R_Kill --> R_Run["Jalankan Job 2"]
        R_Check -- "Tidak" --> R_Run
    end

    style F_Skip stroke:#e74c3c,stroke-width:2px
    style R_Kill stroke:#e74c3c,stroke-width:2px

Kebijakan Toleransi Keterlambatan (startingDeadlineSeconds) #

Jika master node kluster mengalami mati listrik tepat pada saat waktu eksekusi CronJob tiba, CronJob tersebut akan terlewat. Setelah kluster menyala kembali, CronJob Controller akan menghitung berapa kali jadwal terlewat.

  • startingDeadlineSeconds: Menentukan batas waktu toleransi (dalam detik) untuk menjalankan Job yang terlewat. Jika kita mengatur startingDeadlineSeconds: 180, dan kluster pulih 2 menit setelah jadwal terlewat, Job tetap akan dijalankan. Namun jika kluster baru pulih 10 menit kemudian, jadwal tersebut akan dilewati secara permanen demi keamanan data.

Batas 100 Kegagalan Deteksi Jadwal (CronJob Suspension) #

Ada satu aspek internal Kubernetes CronJob Controller yang sangat penting untuk dipahami oleh administrator produksi: Aturan Batas 100 Keterlambatan Jadwal.

Jika kita mendefinisikan CronJob tanpa menentukan nilai startingDeadlineSeconds (atau membiarkannya kosong), dan karena satu atau lain hal (misalnya kluster mengalami mati total, down, atau controller-manager mati) CronJob Controller melewatkan lebih dari 100 jadwal berturut-turut, maka:

  1. CronJob Controller akan berhenti menjadwalkan CronJob tersebut untuk selamanya.
  2. Status CronJob akan ditangguhkan secara pasif, dan kita akan melihat pesan error di log controller seperti: Cannot determine if job needs to be started....

Mengapa Batas Ini Diberlakukan? #

Ini adalah perlindungan memori pada etcd. Tanpa batas 100 ini, jika sebuah CronJob diatur untuk berjalan setiap menit (* * * * *) dan kluster mati selama 5 hari, maka saat menyala kembali, controller akan mencoba membuat lebih dari 7.000 Job secara instan untuk melunasi “utang” jadwal yang terlewat. Hal ini akan langsung membuat kluster crash akibat kehabisan memori.

Solusi Praktis #

To avoid this permanent suspension on CronJob executions running very frequently (e.g. every minute or every 5 minutes), we must always configure startingDeadlineSeconds. By setting this value (e.g. startingDeadlineSeconds: 200), Kubernetes will only check if there are missed schedules within the last 200 seconds. If there is, it will run exactly one Job. This prevents cumulative calculations from breaking the 100 missed schedules threshold. (Untuk menghindari penangguhan permanen ini pada eksekusi CronJob yang berjalan sangat sering, kita wajib selalu mengonfigurasi startingDeadlineSeconds dengan nilai yang sesuai agar aman).


Anti-Pattern dalam Manajemen Job & CronJob #

Kesalahan fatal yang sering kali melumpuhkan kluster produksi saat mengelola tugas batch:

Anti-Pattern 1: Mengabaikan activeDeadlineSeconds (Job Hang Selamanya) #

Mendeploy Job tanpa membatasi durasi eksekusi maksimal.

ANTI-PATTERN: Menjalankan Job database-sync Tanpa activeDeadlineSeconds
// KITA MELAKUKAN:
- Mendeploy Job untuk melakukan sinkronisasi data ke API eksternal.
- API eksternal mengalami gangguan jaringan, mengakibatkan proses di kontainer kita 
  stuck menunggu respon (read timeout) tanpa batas waktu.

// KONSEKUENSI DI PRODUKSI:
- Pod Job akan tetap menyala dan memakan alokasi memori kluster selamanya.
- Karena Job tidak pernah selesai, pipeline deployment CI/CD berikutnya akan hang menunggu konfirmasi.
- Kita membuang biaya komputasi VM cloud untuk kontainer yang sebenarnya tidak melakukan apa-apa.
✓ SOLUSI YANG BENAR:
- Selalu deklarasikan batas waktu realistis pada properti `activeDeadlineSeconds` di tingkat spesifikasi Job:
  spec:
    activeDeadlineSeconds: 300 # Jika dalam 5 menit Job belum selesai, hentikan paksa seluruh proses kontainer
- Tulis penanganan batas waktu (*timeout*) di dalam kode program aplikasi utama Anda.

Anti-Pattern 2: Menjalankan CronJob Sangat Sering Tanpa Membatasi History Limits #

Menggunakan Kubernetes CronJob sebagai scheduler mikro (setiap beberapa detik) dengan menyimpan riwayat tak terbatas.

ANTI-PATTERN: Menulis schedule: "* * * * *" Tanpa successfulJobsHistoryLimit
// KITA MELAKUKAN:
- Menjalankan CronJob pembersihan setiap 1 menit sekali.
- Kita mengabaikan pengisian properti `successfulJobsHistoryLimit` (menggunakan default 3).

// KONSEKUENSI DI PRODUKSI:
- Dalam 1 hari, kluster akan membuat 1440 objek Job dan 1440 Pod Completed.
- Database etcd kluster akan membengkak drastis, menurunkan performa respon API Server Kubernetes secara keseluruhan.
- CLI `kubectl get pods` akan kebanjiran ribuan log Pod Completed yang tidak berguna.
✓ SOLUSI YANG BENAR:
- Selalu batasi secara ketat riwayat penyimpanan Job sukses dan gagal di manifest CronJob:
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 1
- Jika tugas harus dijalankan dalam hitungan detik, jangan gunakan CronJob Kubernetes. 
  Gunakan aplikasi long-running (Deployment) yang mengelola antrean internal (seperti Celery atau BullMQ).

Ringkasan #

  • Filosofi Sekali Selesai — Job digunakan khusus untuk tugas transien yang selesai sekali jalan (run-to-completion), di mana kode keluar exit 0 ditafsirkan sebagai sukses.
  • Aturan restartPolicy Wajib — Tuliskan restartPolicy Never atau OnFailure pada template spec Job; dilarang keras menggunakan tipe Always.
  • Paralelitas Terkendali — Kombinasikan nilai completions (target sukses) dan parallelism (kontainer konkuren) untuk menyusun alur kerja parallel batch processing.
  • Indeksasi Data Efisien — Manfaatkan indeks JOB_COMPLETION_INDEX pada tipe Indexed Job guna membagi rentang pengerjaan data antarworker secara otomatis.
  • Otomatisasi Bersih-Bersih TTL — Konfigurasikan properti ttlSecondsAfterFinished agar kluster secara otomatis menghapus objek Job dan Pod Completed yang mengotori memori.
  • Proteksi Concurrency Forbid — Selalu gunakan kebijakan concurrencyPolicy: Forbid pada CronJob jika tugas berkala tersebut memodifikasi basis data yang sama guna menghindari corrupt data.
  • Amankan Durasi Maksimal — Terapkan properti activeDeadlineSeconds pada setiap Job produksi guna mencegah hang kontainer tanpa batas waktu (infinite timeout).

← Sebelumnya: DaemonSet   Berikutnya: Scheduler Workflow →

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