Strategy & Serial

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 dengan serial: [1, "25%"], semua task di play tetap dalam satu play, dan max_fail_percentage mungkin 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, biasanya forks sudah cukup untuk membatasi konkurensi.
  • serial: 1 untuk zero-downtime rolling deployment — update satu server, verifikasi health, baru lanjut. Cocok untuk service yang butuh perhatian per-server.
  • serial sebagai list ([1, "10%", "50%", "100%"]) untuk canary deployment — ramp-up bertahap dengan verifikasi Prometheus metrics di setiap tahap. Pola paling aman untuk production.
  • serial sebagai persentase ("25%") untuk balance antara kecepatan dan keamanan — cocok untuk kebanyakan service.
  • throttle untuk 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: 0 bersama serial untuk menghentikan deployment segera jika ada kegagalan — tidak ada batch berikutnya jika batch sebelumnya gagal.
  • run_once: true di play dengan serial hanya 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: 0 untuk deploy pertama, naikkan setelah playbook terbukti reliable.
  • Kombinasi strategy: free + throttle: N di task eksternal memberikan kecepatan maksimum tanpa membanjiri resource bersama — best of both worlds.

← Sebelumnya: Jinja2 Lanjutan   Berikutnya: AWX & Tower →

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