Strategy & Serial #
Secara default Ansible menjalankan setiap task di semua host sebelum pindah ke task berikutnya. Ini disebut strategi linear — aman dan mudah diprediksi, tapi tidak selalu optimal. Untuk deployment yang ingin setiap server berjalan secepat mungkin secara independen, atau untuk rolling deployment yang hanya mengupdate beberapa server sekaligus, kita perlu memahami cara mengendalikan alur eksekusi paralel ini. Artikel ini membahas tiga strategy utama Ansible, penggunaan serial untuk rolling deployment, dan throttle untuk membatasi konkurensi per-task. Di akhir, kita akan dapat memilih kombinasi yang tepat untuk setiap skenario deployment — dari full parallel untuk provisioning cloud hingga canary 1-server-at-a-time untuk update ke service kritikal.
Perbandingan Tiga Strategy #
Sebelum masuk ke detail, pahami dulu perbedaan ketiga strategy yang tersedia. Pilihan strategy menentukan kapan Ansible pindah dari satu task ke task berikutnya, dan bagaimana host-host yang berbeda sinkronisasi satu sama lain. Ini bukan pilihan yang sering diubah, tapi keputusan yang salah bisa menyebabkan downtime yang tidak perlu atau bug yang sulit di-debug.
| Strategy | Sinkronisasi | Kecepatan | Debug | Use case |
|---|---|---|---|---|
linear (default) |
Semua host sync per task | Lambat jika ada variasi | Mudah (semua host di stage sama) | Konfigurasi standar, deployment normal |
free |
Tidak ada sinkronisasi | Paling cepat | Sulit (host di stage berbeda) | Task independen per host, provisioning |
host_pinned |
Sync per forks batch |
Medium | Medium | Ketika butuh batas paralel + eksekusi bebas dalam batch |
State diagram berikut menunjukkan perbedaan visual antara ketiganya. Perhatikan bahwa linear adalah satu baris paralel dengan banyak sync point (T1, T2, T3), free adalah banyak baris paralel tanpa sync point, dan host_pinned adalah baris-baris paralel yang dikelompokkan dalam batch:
stateDiagram-v2
[*] --> Linear: default
state Linear {
[*] --> T1_Sync: "Task 1 jalan di semua host"
T1_Sync --> T2_Sync: "Semua host selesai"
T2_Sync --> T3_Sync: "Semua host selesai"
T3_Sync --> [*]
}
state Free {
[*] --> Host1Run: "Host A jalan terus"
Host1Run --> Host2Run: "Host B jalan independen"
Host2Run --> Host3Run: "Host C jalan independen"
Host3Run --> [*]
}
state HostPinned {
[*] --> Batch1: "Batch 1 (5 host) jalan"
Batch1 --> Batch2: "Selesai batch 1, lanjut batch 2"
Batch2 --> [*]
}
Linear strategy adalah default yang masuk akal untuk sebagian besar playbook. Ia menjamin bahwa jika task ke-3 di host A butuh output task ke-2, task ke-2 sudah selesai. Tapi untuk skenario tertentu — deployment ke cluster besar, provisioning cloud, atau workload yang berbeda jauh antar-host — strategy lain memberikan keuntungan signifikan.
Linear Strategy: Default yang Aman #
Linear strategy menjalankan satu task di semua host, menunggu semua selesai, baru lanjut ke task berikutnya. Ini adalah default Ansible karena alasan yang bagus: ini memberikan prediktabilitas yang tinggi. Setiap host melewati task dalam urutan yang sama, dan output log mudah di-follow karena semua host ada di task yang sama.
# Default strategy: linear (tidak perlu ditulis eksplisit)
- name: Konfigurasi semua webserver
hosts: webservers
# strategy: linear # implicit
tasks:
- name: Update package index
apt:
update_cache: yes
- name: Install nginx
apt:
name: nginx
state: present
- name: Copy konfigurasi
copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
Skenario ideal untuk linear: konfigurasi aplikasi, deployment yang butuh update atomik di semua server, atau task yang punya dependensi kuat antar-task. Konfigurasi cluster database, di mana setiap node harus update schema dulu sebelum node lain boleh join, adalah contoh klasik linear strategy.
Kelemahan linear: jika ada 100 host dan 1 host lambat, semua host lain menunggu. Total waktu eksekusi = (waktu task terlambat) × (jumlah task). Untuk cluster 100 server dengan waktu task 30 detik, deployment selesai dalam 5 menit hanya karena 1 server lambat. Ini yang coba dipecahkan strategy lain.
Free Strategy: Setiap Host Berjalan Terus #
Free strategy membebaskan setiap host dari kewajiban menunggu host lain. Begitu host menyelesaikan task ke-N, ia langsung lanjut ke task ke-(N+1), tanpa peduli di mana host lain berada. Ini ideal untuk task yang tidak bergantung pada output host lain — provisioning, download artifact, atau konfigurasi yang sepenuhnya lokal.
# playbooks/deploy-with-free.yml
---
- name: Deploy dengan free strategy
hosts: webservers
strategy: free # Setiap host berjalan independen
tasks:
- name: Pull kode
git:
repo: https://github.com/company/app.git
dest: /opt/app
version: "{{ version }}"
- name: Install dependencies
pip:
requirements: /opt/app/requirements.txt
virtualenv: /opt/app/venv
# Setiap host langsung lanjut ke task ini setelah pull selesai
# tanpa menunggu host lain selesai pull
- name: Compile assets
command: npm run build
args:
chdir: /opt/app
ANTI-PATTERN vs BENAR: Free vs Linear untuk Deployment #
# ANTI-PATTERN: linear untuk deployment yang bisa free (kehilangan 50% waktu)
- name: Deploy ke 100 server
hosts: webservers
strategy: linear # Semua host tunggu satu sama lain
tasks:
- name: Download artifact (10 detik di server cepat, 60 detik di lambat)
get_url:
url: "https://artifacts.company.com/app-{{ version }}.tar.gz"
dest: /tmp/app.tar.gz
# Total waktu: 60 detik (tunggu server paling lambat)
# Lalu 100 host install paralel di task berikutnya
# BENAR: free strategy untuk task yang tidak butuh sinkronisasi
- name: Deploy ke 100 server
hosts: webservers
strategy: free # Setiap host langsung lanjut
tasks:
- name: Download artifact
get_url:
url: "https://artifacts.company.com/app-{{ version }}.tar.gz"
dest: /tmp/app.tar.gz
- name: Install (host yang cepat langsung mulai sambil yang lain download)
unarchive:
src: /tmp/app.tar.gz
dest: /opt/app
remote_src: yes
# Total waktu: 60 detik (server paling lambat) — TAPI instalasi paralel
# dan tidak ada jeda antar-task
Free strategy menyulitkan debug. Karena host bisa di task yang berbeda, saat satu host gagal, kita harus tahu di task mana host itu gagal relatif terhadap host lain. Output log juga lebih sulit di-follow. Gunakan free hanya untuk playbook yang sudah stabil dan diuji, atau tambahkan serial agar batch terkontrol.
serial: Rolling Deployment #
serial mengontrol berapa banyak host yang diproses sekaligus dalam satu “batch”. Setelah satu batch selesai (semua task di semua host dalam batch), Ansible pindah ke batch berikutnya. Ini adalah cara standar untuk rolling deployment yang aman: update beberapa server sekaligus, verifikasi, baru lanjut.
# playbooks/rolling-deploy.yml
---
- name: Rolling deployment — 1 server sekaligus
hosts: webservers
serial: 1 # Update satu server, tunggu selesai, baru lanjut ke berikutnya
tasks:
- name: Deploy kode baru
git:
repo: https://github.com/company/app.git
dest: /opt/app
version: "{{ version }}"
- name: Restart aplikasi
systemd:
name: myapp
state: restarted
- name: Verifikasi health
uri:
url: "http://localhost:{{ app_port }}/health"
status_code: 200
retries: 6
delay: 5
serial bisa berupa angka, persentase, atau list untuk ramp-up bertahap:
# Serial sebagai persentase
- hosts: webservers
serial: "25%" # Update 25% server sekaligus (misal: 2 dari 8 server)
# Serial sebagai list: canary deployment
- hosts: webservers
serial:
- 1 # Batch 1: satu server saja (canary)
- "10%" # Batch 2: 10% dari sisa
- "50%" # Batch 3: 50% dari sisa
- "100%" # Batch 4: semua yang tersisa
Pola list ini sangat berguna untuk canary deployment — deploy ke satu server dulu, verifikasi, baru lanjut ke lebih banyak server. Ini adalah pola yang dipakai oleh Netflix, Google, dan banyak perusahaan teknologi besar untuk deployment yang tidak boleh gagal.
Sequence Diagram Serial vs Linear #
Untuk memahami apa yang terjadi secara internal dengan serial, lihat sequence diagram berikut. Perhatikan bagaimana serial: 2 membagi 4 host menjadi 2 batch, dan tiap batch menunggu selesai sebelum lanjut:
sequenceDiagram
participant Ctrl as "Ansible Controller"
participant H1 as "Host 1"
participant H2 as "Host 2"
participant H3 as "Host 3"
participant H4 as "Host 4"
Note over Ctrl,H1: Batch 1 (serial=2)
Ctrl->>H1: Task 1
Ctrl->>H2: Task 1
H1-->>Ctrl: Done
H2-->>Ctrl: Done
Ctrl->>H1: Task 2
Ctrl->>H2: Task 2
H1-->>Ctrl: Done
H2-->>Ctrl: Done
Note over Ctrl,H3: Batch 2
Ctrl->>H3: Task 1
Ctrl->>H4: Task 1
H3-->>Ctrl: Done
H4-->>Ctrl: Done
Ctrl->>H3: Task 2
Ctrl->>H4: Task 2
H3-->>Ctrl: Done
H4-->>Ctrl: Done
Sequence ini menjelaskan konsep penting: dengan serial: 2 dan 4 host, total waktu eksekusi = 2 × (waktu per batch). Dengan linear, semua 4 host diproses paralel dalam 1 batch. Trade-off-nya: serial lebih aman (satu batch gagal tidak menggugurkan semua), tapi lebih lambat. Untuk deployment production, trade-off ini biasanya sangat berharga.
Pola Canary Deployment #
Canary deployment adalah pola deployment yang paling aman untuk service kritikal. Ide dasarnya: deploy ke subset kecil dulu, verifikasi bahwa service tetap sehat, baru deploy ke lebih banyak. Jika canary gagal, deployment berhenti dan hanya 1-2 server yang terdampak — bukan semua.
# playbooks/canary-deploy.yml
---
- name: Canary deployment — tahap 1 (1 server)
hosts: webservers
serial: 1
max_fail_percentage: 0 # Hentikan jika canary gagal
tasks:
- name: Deploy ke canary server
git:
repo: https://github.com/company/app.git
dest: /opt/app
version: "{{ version }}"
- name: Restart aplikasi
systemd:
name: myapp
state: restarted
- name: Tunggu dan verifikasi canary
uri:
url: "http://localhost:{{ app_port }}/health"
status_code: 200
retries: 12
delay: 10
- name: Verifikasi error rate canary tidak meningkat
uri:
url: "http://prometheus.internal/api/v1/query"
method: GET
body_format: form-urlencoded
body: >-
query=rate(http_requests_total{status=~"5..",instance="{{ inventory_hostname }}"}[5m])
register: error_rate
failed_when:
- error_rate.json.data.result | length > 0
- error_rate.json.data.result[0].value[1] | float > 0.01
- name: Deploy ke sisa server setelah canary OK
hosts: webservers[1:] # Semua kecuali server pertama (canary)
serial: "25%"
tasks:
- name: Deploy ke sisa server
git:
repo: https://github.com/company/app.git
dest: /opt/app
version: "{{ version }}"
- name: Restart aplikasi
systemd:
name: myapp
state: restarted
Pola canary punya tiga elemen yang masing-masing krusial. Pertama, serial: 1 untuk batch pertama — deploy ke satu server saja. Kedua, max_fail_percentage: 0 — hentikan deployment jika canary gagal, jangan langsung roll out. Ketiga, verifikasi error rate dari metrics system (Prometheus, Datadog, dll.) — health check saja tidak cukup karena error bisa terjadi di background tanpa permukaan di health check.
Pisahkan canary dan roll out dalam dua play. Pola di atas menggunakan dua play terpisah — play pertama untuk canary dengan verifikasi, play kedua untuk sisanya. Ini memastikan verifikasi selesai sebelum deployment dilanjutkan. Jika kita menggabungkan dalam satu play denganserial: [1, "25%"], semua task di play tetap dalam satu play, danmax_fail_percentagemungkin tidak berlaku antar batch.
throttle: Batasi Konkurensi di Level Task #
serial mengontrol batch di level play. throttle membatasi konkurensi di level task individual — berguna untuk task yang membebani resource eksternal. Perbedaan ini penting: serial mempengaruhi semua task dalam play, throttle hanya mempengaruhi satu task. Kombinasi keduanya memberikan kontrol granular.
# playbooks/deploy-with-throttle.yml
---
- name: Deploy ke semua server
hosts: webservers
strategy: free # Setiap host berjalan secepat mungkin
tasks:
- name: Pull kode (bisa paralel sepenuhnya)
git:
repo: https://github.com/company/app.git
dest: /opt/app
version: "{{ version }}"
- name: Download artifact dari S3 (batasi 5 download sekaligus)
aws_s3:
bucket: my-artifacts
object: "releases/{{ version }}/app.tar.gz"
dest: /tmp/app.tar.gz
mode: get
throttle: 5 # Maksimal 5 host menjalankan task ini bersamaan
# Mencegah throttling dari S3 atau membanjiri bandwidth
- name: Restart aplikasi (bisa paralel sepenuhnya)
systemd:
name: myapp
state: restarted
ANTI-PATTERN vs BENAR: Throttle untuk Resource Eksternal #
# ANTI-PATTERN: 100 host download paralel dari S3 — kena rate limit
- name: Download artifact
aws_s3:
bucket: my-artifacts
object: "releases/{{ version }}/app.tar.gz"
dest: /tmp/app.tar.gz
mode: get
# Tanpa throttle, S3 akan throttle atau bandwidth lokal jenuh
# BENAR: throttle 5-10 download bersamaan
- name: Download artifact
aws_s3:
bucket: my-artifacts
object: "releases/{{ version }}/app.tar.gz"
dest: /tmp/app.tar.gz
mode: get
throttle: 10
# Bandwidth seimbang, S3 tidak throttled, deployment tetap cepat
Throttle vs forks: forks (di ansible.cfg) mengontrol konkurensi global Ansible. throttle mengontrol per-task. Jika kita menetapkan forks = 50 dan throttle: 5 di task S3 download, maksimum 50 host berjalan paralel, tetapi hanya 5 yang mendownload dari S3 sekaligus. Ini sangat berguna untuk membedakan antara “berapa host yang ansible proses sekaligus” dan “berapa host yang boleh menggunakan resource eksternal sekaligus”.
Jangan set throttle: 0 atau hapus untuk production. Throttle default adalah jumlah host di play, yang berarti tanpa throttle semua host berjalan paralel. Ini bisa menghambat API pihak ketiga, S3 bucket, atau database. Selalu set throttle eksplisit untuk task yang berbicara dengan resource eksternal bersama. Untuk task yang hanya local (file, systemd, package manager), throttle tidak perlu.
max_fail_percentage: Batas Toleransi Kegagalan #
max_fail_percentage adalah pengaman untuk serial deployment. Tanpa parameter ini, Ansible akan lanjut ke batch berikutnya meskipun batch sebelumnya gagal total. Dengan max_fail_percentage: 0, deployment berhenti pada kegagalan pertama.
- name: Rolling deploy dengan toleransi kegagalan
hosts: appservers
serial: 2
max_fail_percentage: 25 # Toleransi 25% host gagal per batch
tasks:
- name: Deploy kode
git:
repo: https://github.com/company/app.git
dest: /opt/app
version: "{{ version }}"
- name: Restart aplikasi
systemd:
name: myapp
state: restarted
register: restart_result
until: restart_result is success
retries: 3
delay: 5
Cara kerja max_fail_percentage: Ansible menghitung persentase host yang gagal di batch saat ini. Jika lebih dari threshold, seluruh play dihentikan. Untuk batch 4 host dengan max_fail_percentage: 25, jika 2 host gagal, play dihentikan. Untuk batch 1 host (canary), threshold harus 0 karena 100% = 1 host = selalu di atas threshold 25.
run_once dengan Serial #
Saat menggunakan serial, task dengan run_once hanya berjalan sekali di batch pertama, bukan di setiap batch. Ini berguna untuk migrasi database, update DNS, atau operasi satu-kali yang hanya perlu dilakukan sekali di awal deployment.
- name: Rolling deploy dengan migrasi database
hosts: appservers
serial: 2
pre_tasks:
- name: Jalankan migrasi database (hanya sekali, di awal)
command: python manage.py migrate
run_once: true # Hanya di batch pertama, server pertama
delegate_to: "{{ groups['appservers'][0] }}"
tasks:
- name: Deploy kode ke semua server (per batch)
git:
repo: https://github.com/company/app.git
dest: /opt/app
version: "{{ version }}"
- name: Jalankan migrasi ringan per server
command: python manage.py migrate_light
# Ini jalan di setiap server, setiap batch
Yang perlu diperhatikan: run_once di dalam play dengan serial hanya jalan sekali total di play tersebut. Jika kita menginginkan migrasi berjalan per batch, hapus run_once. Kombinasi run_once + delegate_to berguna untuk operasi yang hanya boleh dilakukan di satu titik (misal: primary database), bukan di setiap server.
Decision Tree Memilih Strategy #
Dengan semua opsi yang tersedia, kita mungkin bingung kapan harus pakai yang mana. Decision tree berikut merangkum heuristik yang bisa kita gunakan sebagai titik awal. Tentu saja, setiap environment memiliki batasan sendiri yang harus kita pertimbangkan:
flowchart TD
Start["Deployment"] --> Q1{"Butuh zero-downtime?"}
Q1 -- "Tidak" --> Linear["Linear strategy, no serial"]
Q1 -- "Ya" --> Q2{"Task independent per host?"}
Q2 -- "Ya" --> Free["Free strategy"]
Q2 -- "Tidak" --> Q3{"Risiko kegagalan per server tinggi?"}
Q3 -- "Ya" --> Q4{"Bisa rollback otomatis?"}
Q4 -- "Tidak" --> Canary["serial: 1, max_fail_percentage 0"]
Q4 -- "Ya" --> Q5{"Service kritikal production?"}
Q5 -- "Ya" --> Canary2["serial: list, 1/10%/50%/100%"]
Q5 -- "Tidak" --> Roll["serial: 25% atau 50%"]
Q3 -- "Tidak" --> Roll
Mulai konservatif, longgarkan setelah yakin. Pola yang sehat untuk deploy service baru: mulai dengan serial: 1 dan max_fail_percentage: 0 sampai kita yakin playbook-nya reliable. Setelah beberapa deploy sukses tanpa rollback, naikkan ke serial: 10% atau serial: 25%. Untuk canary murni, serial: [1, "10%", "50%", "100%"] adalah sweet spot untuk kebanyakan service.
Kapan Tidak Perlu Strategy atau Serial #
Terakhir, penting untuk diketahui bahwa tidak semua playbook butuh strategy lanjutan. Untuk task yang idempotent dan berisiko rendah, default linear + semua host paralel sudah cukup:
Gunakan DEFAULT (linear, semua host paralel) untuk:
✓ Provisioning awal server baru
✓ Update package security di fleet
✓ Konfigurasi service non-kritikal
✓ Task idempotent dengan rollback mudah
Gunakan SERIAL + max_fail_percentage untuk:
✓ Deployment aplikasi ke production
✓ Update database schema
✓ Restart service kritikal
✓ Perubahan yang butuh verifikasi per batch
Gunakan THROTTLE untuk:
✓ Download dari S3 / object storage
✓ API call ke service dengan rate limit
✓ Task yang membebani database bersama
✓ Provisioning ke cloud API (AWS, GCP, Azure)
Gunakan FREE strategy untuk:
✓ Provisioning cloud dengan waktu per host bervariasi
✓ Deployment dengan task yang fully independent
✓ Task yang di-chained per host (A → B → C dengan B butuh output A)
Ringkasan #
linear(default): semua host menyelesaikan satu task sebelum lanjut — mudah diprediksi dan di-debug, tapi host yang cepat menunggu host yang lambat. Cocok untuk konfigurasi standar dan deployment normal.free: setiap host berjalan secepat mungkin tanpa menunggu host lain — paling cepat, tapi lebih sulit di-debug karena host bisa di stage berbeda. Cocok untuk task yang fully independent seperti provisioning cloud.host_pinned: seperti free tapi dalam batch — jarang dipakai, biasanyaforkssudah cukup untuk membatasi konkurensi.serial: 1untuk zero-downtime rolling deployment — update satu server, verifikasi health, baru lanjut. Cocok untuk service yang butuh perhatian per-server.serialsebagai list ([1, "10%", "50%", "100%"]) untuk canary deployment — ramp-up bertahap dengan verifikasi Prometheus metrics di setiap tahap. Pola paling aman untuk production.serialsebagai persentase ("25%") untuk balance antara kecepatan dan keamanan — cocok untuk kebanyakan service.throttleuntuk membatasi konkurensi di level task individual — gunakan untuk download dari S3, API call ke service dengan rate limit, atau task yang membebani database bersama.max_fail_percentage: 0bersamaserialuntuk menghentikan deployment segera jika ada kegagalan — tidak ada batch berikutnya jika batch sebelumnya gagal.run_once: truedi play denganserialhanya jalan sekali di batch pertama — berguna untuk migrasi database atau update DNS yang one-time.- Mulai konservatif, longgarkan setelah yakin. Pola sehat:
serial: 1+max_fail_percentage: 0untuk deploy pertama, naikkan setelah playbook terbukti reliable.- Kombinasi
strategy: free+throttle: Ndi task eksternal memberikan kecepatan maksimum tanpa membanjiri resource bersama — best of both worlds.