Service

Service #

Menginstal paket perangkat lunak di server barulah separuh dari proses penyusunan infrastruktur. Setelah aplikasi terinstal, kita harus memastikan layanan (service) tersebut berjalan, dikonfigurasi dengan benar, dan dapat memulihkan diri setelah server mengalami restart. Pada sistem operasi Linux modern, systemd telah menjadi standar de facto untuk sistem inisialisasi (init system) dan manajemen service. Ansible mempermudah pengelolaan ini melalui modul ansible.builtin.systemd dan ansible.builtin.service. Dengan pemahaman yang mendalam tentang siklus hidup service di systemd, kita dapat memastikan uptime aplikasi kita tetap terjaga tanpa melakukan tindakan restart yang tidak perlu.


Mengelola Lifecycle Service: Perbedaan Modul service dan systemd #

Di dalam Ansible, kita akan menemukan dua modul utama yang tampaknya memiliki fungsi yang sama: ansible.builtin.service dan ansible.builtin.systemd. Memahami perbedaan keduanya sangat penting untuk menentukan modul mana yang paling cocok kita gunakan dalam skenario tertentu.

Modul service adalah modul bawaan tingkat tinggi yang bersifat generik. Modul ini dirancang untuk kompatibilitas lintas sistem operasi. Di latar belakang, ia mendeteksi sistem inisialisasi apa yang sedang berjalan di server target (apakah SysVinit kuno, Upstart milik Ubuntu versi lama, systemd, atau OpenRC di Alpine) dan menerjemahkan instruksi kita ke perkakas yang sesuai. Modul ini sangat ideal jika kita membuat playbook generik yang harus bekerja di sistem yang sangat lama atau sistem alternatif seperti Alpine Linux.

Sementara itu, modul systemd adalah modul khusus (native) yang dirancang secara eksklusif untuk berkomunikasi langsung dengan manajer sistem systemd melalui antarmuka systemctl. Karena berfokus pada satu sistem inisialisasi, modul systemd menyediakan akses ke parameter-parameter lanjutan yang tidak dimiliki oleh modul generik service. Beberapa parameter penting tersebut meliputi daemon_reload, masked, scope, user, dan no_block.

Karena hampir semua distribusi Linux server modern saat ini (seperti Ubuntu 16.04+, Debian 8+, RHEL 7+, Rocky Linux, dan AlmaLinux) menggunakan systemd sebagai standar bawaannya, sangat disarankan untuk menggunakan modul systemd secara langsung untuk mendapatkan fitur kontrol yang lebih kaya dan presisi.

flowchart TD
    A["Layanan Baru Terinstal"] --> B{"Tindakan yang Diinginkan?"}
    B -- "Jalankan Layanan" --> C["state: started"]
    B -- "Aktifkan saat Boot" --> D["enabled: true"]
    B -- "Matikan Layanan" --> E["state: stopped"]
    B -- "Nonaktifkan saat Boot" --> F["enabled: false"]
    B -- "Matikan Total (Blokir)" --> G["masked: true"]
    C --> H["Layanan Aktif (Running)"]
    D --> I["Layanan Aktif Setelah Reboot"]
    E --> J["Layanan Mati (Inactive)"]
    F --> K["Layanan Tidak Mulai Setelah Reboot"]
    G --> L["Layanan Dikunci (Masked) - Tidak Bisa Dijalankan"]

Parameter Esensial Modul systemd #

Untuk mengendalikan keadaan layanan, modul systemd menyediakan beberapa opsi penting pada parameter state dan enabled. Berikut adalah penjelasan mendalam tentang opsi-opsi tersebut:

  • state: started: Memastikan layanan berjalan. Jika layanan mati, Ansible akan menyalakannya. Jika layanan sudah berjalan, Ansible tidak melakukan tindakan apa pun (idempoten).
  • state: stopped: Memastikan layanan mati. Jika layanan berjalan, Ansible akan menghentikannya.
  • state: restarted: Memaksa layanan untuk berhenti terlebih dahulu, lalu berjalan kembali. Tindakan ini tidak idempoten karena Ansible akan selalu melakukan restart setiap kali playbook dijalankan.
  • state: reloaded: Mengirimkan sinyal (biasanya SIGHUP) ke proses layanan untuk membaca ulang file konfigurasi di memori tanpa mematikan proses utama. Seperti halnya restarted, opsi ini juga tidak idempoten dan akan dijalankan setiap kali dipanggil.
  • enabled: true: Memastikan layanan terdaftar di systemd untuk berjalan secara otomatis ketika server melakukan proses booting.
  • enabled: false: Memastikan layanan tidak akan berjalan otomatis saat server booting.
  • masked: true: Ini adalah status khusus yang mengunci layanan secara total dengan cara membuat tautan simbolik (symlink) dari unit file layanan tersebut ke /dev/null. Sesuatu layanan yang di-mask tidak akan bisa dinyalakan, baik secara manual lewat systemctl start maupun secara otomatis karena dipicu oleh dependensi layanan lain. Ini sangat berguna jika kita ingin memastikan layanan bawaan yang berisiko (seperti telnet atau layanan firewall bawaan yang bentrok) benar-benar dinonaktifkan secara permanen.

Mari kita lihat implementasi penggunaan parameter ini dalam playbook:

# Memastikan layanan database postgresql berjalan dan aktif saat boot
- name: Jalankan dan aktifkan PostgreSQL
  systemd:
    name: postgresql
    state: started
    enabled: true

# Menghentikan dan menonaktifkan layanan yang berisiko bentrok dengan Nginx
- name: Matikan dan nonaktifkan Apache2
  systemd:
    name: apache2
    state: stopped
    enabled: false

# Memblokir total layanan agar tidak bisa dinyalakan oleh proses lain
- name: Blokir (mask) layanan snapd daemon
  systemd:
    name: snapd
    masked: true
    state: stopped

Perbedaan Signifikan: restarted vs reloaded #

Salah satu kesalahan umum dalam administrasi sistem menggunakan Ansible adalah menggunakan state: restarted secara berlebihan untuk menerapkan perubahan konfigurasi. Kita harus memahami konsekuensi dari kedua tindakan ini terhadap ketersediaan layanan (availability) di lingkungan produksi.

Tindakan restarted bekerja dengan cara menghentikan proses aplikasi sepenuhnya (SIGTERM/SIGKILL), melepas socket jaringan, membersihkan memori, lalu memulai ulang proses dari awal. Selama proses mematikan dan menyalakan kembali ini (yang bisa memakan waktu dari beberapa milidetik hingga beberapa detik), server kita tidak dapat menerima koneksi baru dan koneksi aktif yang sedang berjalan akan terputus. Hal ini menciptakan celah downtime bagi pengguna aplikasi kita.

Tindakan reloaded bekerja dengan cara yang jauh lebih elegan. Alih-alih mematikan proses, systemd mengirimkan sinyal reload konfigurasi ke proses yang berjalan. Proses utama aplikasi akan membaca kembali file konfigurasi dari disk, memvalidasinya, memuat ulang konfigurasi baru ke memori, dan menghentikan worker lama secara bertahap (graceful shutdown) setelah worker baru siap menerima trafik. Koneksi aktif yang sedang berjalan tetap dilayani tanpa terputus, sehingga kita mendapatkan reload konfigurasi tanpa downtime sama sekali (zero-downtime reload).

Mari kita lihat skenario perbandingan kapan kita harus menggunakan restarted dan reloaded:

# BENAR: Menggunakan reloaded untuk web server yang melayani trafik pengguna aktif
- name: Reload konfigurasi Nginx tanpa downtime
  systemd:
    name: nginx
    state: reloaded

# BENAR: Menggunakan restarted untuk runtime bahasa yang tidak mendukung hot reload
- name: Restart aplikasi Python Gunicorn (membutuhkan restart penuh)
  systemd:
    name: myapp-gunicorn
    state: restarted

Sebagai aturan praktis, jika aplikasi mendukung hot reload konfigurasi (seperti Nginx, HAProxy, PostgreSQL, Apache, dan SSH), gunakanlah reloaded. Jika aplikasi tidak mendukung fitur tersebut (seperti Node.js, Go binaries, atau Java Spring Boot), barulah kita terpaksa menggunakan restarted.


Automating Daemon Reload after Unit File Changes #

Di lingkungan modern, kita tidak hanya mengelola service bawaan sistem operasi, tetapi juga sering men-deploy aplikasi kustom kita sendiri. Untuk menjalankan aplikasi kustom sebagai service, kita perlu menulis file deskripsi layanan yang disebut systemd unit file (biasanya disimpan dengan ekstensi .service di direktori /etc/systemd/system/).

Setiap kali kita membuat unit file baru atau memodifikasi unit file yang sudah ada (misalnya mengubah limit memori, mengubah executable path, atau menambahkan variabel lingkungan baru), systemd tidak akan mengetahui perubahan tersebut secara otomatis. Jika kita langsung mencoba menjalankan service, systemd akan memicu peringatan (warning) bahwa file konfigurasi di disk berbeda dengan konfigurasi yang dimuat di memori daemon systemd.

Untuk memberitahu systemd bahwa ada unit file yang berubah, kita harus menjalankan operasi daemon reload (setara dengan systemctl daemon-reload). Di Ansible, operasi ini diintegrasikan secara aman menggunakan parameter daemon_reload: true di dalam modul systemd.

Mari kita lihat perbedaan antara anti-pattern yang tidak efisien dan solusi deklaratif yang benar:

# ANTI-PATTERN: Menjalankan daemon-reload menggunakan shell command di setiap Ansible run
- name: Reload systemd daemon secara manual
  shell: systemctl daemon-reload
  changed_when: true

# BENAR: Menggunakan daemon_reload di dalam handler yang dipicu oleh perubahan unit file
- name: Deploy custom service unit file
  template:
    src: templates/myapp.service.j2
    dest: /etc/systemd/system/myapp.service
    owner: root
    group: root
    mode: '0644'
  notify: Reload daemon dan restart service

# Definisikan handler di bagian bawah playbook kita
handlers:
  - name: Reload daemon dan restart service
    systemd:
      name: myapp
      state: restarted
      daemon_reload: true
      enabled: true

Dalam pendekatan yang benar, operasi daemon_reload: true hanya akan dijalankan jika file myapp.service mengalami perubahan nyata. Jika file template di control node sama dengan file yang ada di managed node, task template akan melaporkan status ok (bukan changed), handler tidak akan dipicu, dan kita terhindar dari pemborosan sumber daya server untuk memuat ulang daemon systemd.


Mengoordinasikan Perubahan Konfigurasi dengan Handlers #

Handlers adalah salah satu fitur paling penting di Ansible untuk menjaga kestabilan server target. Handlers pada dasarnya adalah task biasa, namun mereka tidak akan dieksekusi kecuali jika dipicu secara eksplisit oleh task lain menggunakan instruksi notify.

Berikut adalah aturan-aturan penting terkait perilaku Handlers yang harus kita pahami:

  1. Eksekusi di Akhir Play: Secara default, semua handler yang dipicu (notified) selama play akan dikumpulkan dan dijalankan bersama-sama di akhir play, tepat setelah seluruh task utama selesai dijalankan. Ini mencegah service mengalami restart berkali-kali di tengah jalan jika ada banyak perubahan file konfigurasi yang memicunya.
  2. Hanya Berjalan pada Status Changed: Handler hanya akan berjalan jika task yang memanggilnya mengembalikan status changed. Jika task mengembalikan status ok (karena konfigurasi di server sudah sesuai dengan desired state), handler tidak akan dijalankan. Hal ini sangat penting untuk menjamin sifat idempoten.
  3. Eksekusi Tunggal: Jika ada lima task berbeda yang memodifikasi konfigurasi Nginx dan semuanya memanggil handler Restart Nginx yang sama, Ansible hanya akan menjalankan handler tersebut sebanyak satu kali di akhir play. Kita terhindar dari downtime berulang akibat restart yang tidak perlu.
  4. Pembatalan Akibat Error: Jika playbook mengalami kegagalan (error) di tengah jalan sebelum mencapai akhir play, seluruh handler yang sudah terdaftar tidak akan dieksekusi. Ini adalah langkah pengamanan untuk mencegah service menyala kembali dengan konfigurasi yang setengah jadi atau rusak.

Mari kita lihat contoh implementasi koordinasi yang kompleks:

- name: Konfigurasi web server Nginx
  hosts: webservers
  become: true
  
  tasks:
    - name: Deploy file konfigurasi utama nginx.conf
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: Reload Nginx

    - name: Deploy vhost konfigurasi aplikasi
      template:
        src: templates/vhost.conf.j2
        dest: /etc/nginx/sites-available/myapp.conf
      notify: Reload Nginx

    - name: Buat symlink untuk mengaktifkan vhost
      file:
        src: /etc/nginx/sites-available/myapp.conf
        dest: /etc/nginx/sites-enabled/myapp.conf
        state: link
      notify: Reload Nginx

  handlers:
    - name: Reload Nginx
      systemd:
        name: nginx
        state: reloaded

Dalam skenario di atas, meskipun ada tiga task berbeda yang dapat memicu Reload Nginx, Nginx hanya akan menerima sinyal reload sebanyak satu kali di akhir play setelah semua file konfigurasi utama, vhost, dan symlink terpasang dengan sempurna.


Ketika kita menginstruksikan Ansible untuk menyalakan suatu service menggunakan state: started, Ansible hanya mendeteksi apakah proses utama service tersebut berhasil dipicu oleh systemd. Ansible tidak mengetahui apakah aplikasi di dalam service tersebut sudah selesai melakukan inisialisasi memori, memuat file konfigurasi internal, atau menghubungkan diri ke database.

Sebagai contoh, ketika service Java Spring Boot atau PostgreSQL dijalankan, proses systemd akan langsung melaporkan status active (running). Namun, aplikasi tersebut mungkin membutuhkan waktu 10 hingga 30 detik untuk benar-benar siap mendengarkan port jaringan dan menerima trafik. Jika playbook kita langsung melanjutkan ke task berikutnya (seperti menjalankan migrasi database atau pengujian endpoint API) tanpa menunggu aplikasi benar-benar siap, task berikutnya tersebut dipastikan akan gagal karena koneksi ditolak.

Untuk menjembatani jeda inisialisasi ini, kita harus menggunakan modul verifikasi kesehatan seperti ansible.builtin.wait_for atau ansible.builtin.uri.

# Skenario 1: Menunggu port database terbuka sebelum menjalankan migrasi
- name: Restart PostgreSQL
  systemd:
    name: postgresql
    state: restarted

- name: Tunggu hingga PostgreSQL siap menerima koneksi di port 5432
  wait_for:
    port: 5432
    host: 127.0.0.1
    delay: 3        # Tunggu 3 detik dulu sebelum mulai melakukan pengecekan pertama
    timeout: 45     # Batasi waktu tunggu maksimal 45 detik, batalkan jika lewat
    state: started  # Pastikan port berstatus "open"

- name: Jalankan migrasi database aplikasi kita
  command: /opt/myapp/venv/bin/python manage.py migrate

# Skenario 2: Melakukan polling pada HTTP Health Endpoint aplikasi
- name: Restart API backend service
  systemd:
    name: myapp-backend
    state: restarted

- name: Lakukan polling ke healthcheck endpoint hingga mengembalikan HTTP 200
  uri:
    url: "http://127.0.0.1:8080/api/health"
    status_code: 200
  register: api_health
  until: api_health.status == 200
  retries: 15      # Lakukan percobaan maksimal 15 kali
  delay: 2         # Jeda waktu antar percobaan adalah 2 detik

Dengan menerapkan langkah verifikasi ini, playbook kita menjadi jauh lebih tangguh karena memastikan keadaan server target benar-benar siap secara fungsional sebelum melanjutkan langkah konfigurasi berikutnya.


Memeriksa Status Layanan secara Idempoten #

Dalam beberapa kasus, kita hanya ingin memeriksa apakah suatu service berjalan atau tidak, tanpa ingin mengubah kondisinya (misalnya untuk kebutuhan monitoring atau pelaporan). Untuk melakukan hal ini secara aman tanpa memicu perubahan status task di Ansible, kita harus meregistrasikan output pemeriksaan systemd dan menyertakan parameter changed_when: false.

- name: Periksa status keaktifan service Docker
  systemd:
    name: docker
  register: docker_service_info
  changed_when: false  # Task ini tidak mengubah apa pun, pastikan selalu hijau (ok)

- name: Tampilkan status internal Docker
  debug:
    msg: >
      Docker ActiveState: {{ docker_service_info.status.ActiveState }}
      Docker LoadState: {{ docker_service_info.status.LoadState }}
      Docker SubState: {{ docker_service_info.status.SubState }}      

- name: Ambil tindakan bersyarat jika Docker mati
  debug:
    msg: "PERINGATAN: Layanan Docker tidak aktif di host ini!"
  when: docker_service_info.status.ActiveState != "active"

Properti status yang dikembalikan oleh modul systemd berisi representasi lengkap dari objek D-Bus systemd untuk service tersebut, memberikan kita informasi yang sangat mendalam tanpa harus parsing output teks mentah dari perintah shell.


Studi Kasus: Deploy Service Node.js Kustom dengan systemd #

Mari kita satukan seluruh pemahaman kita ke dalam sebuah skenario nyata: men-deploy aplikasi Node.js kustom sebagai service systemd, memastikannya berjalan dengan restart policy yang aman, mengonfigurasi auto-start saat boot, memicu daemon reload jika unit file berubah, dan memverifikasi kesehatan port sebelum mengakhiri proses deployment.

Berikut adalah template file service yang dinamis (templates/nodejs-app.service.j2):

{# templates/nodejs-app.service.j2 #}
[Unit]
Description=Layanan Backend Node.js {{ app_name }}
After=network.target mongodb.service
Requires=mongodb.service

[Service]
Type=simple
User={{ app_user }}
Group={{ app_group }}
WorkingDirectory={{ app_dir }}
Environment=NODE_ENV={{ app_env }}
Environment=PORT={{ app_port }}
ExecStart=/usr/bin/node {{ app_dir }}/server.js
Restart=always
RestartSec=5
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=nodejs-{{ app_name }}

[Install]
WantedBy=multi-user.target

Dan berikut adalah playbook lengkap yang melakukan otomasi deployment service tersebut:

# playbooks/deploy-nodejs-service.yml
---
- name: Deploy Service Node.js secara Mandiri
  hosts: appservers
  become: true
  
  vars:
    app_name: "payment-api"
    app_user: "nodeapp"
    app_group: "nodeapp"
    app_dir: "/var/www/payment-api"
    app_env: "production"
    app_port: 3000

  tasks:
    - name: Pastikan user dan grup aplikasi Node.js sudah ada
      block:
        - name: Buat grup aplikasi
          group:
            name: "{{ app_group }}"
            state: present

        - name: Buat user aplikasi non-interaktif
          user:
            name: "{{ app_user }}"
            group: "{{ app_group }}"
            shell: /usr/sbin/nologin
            create_home: false
            state: present

    - name: Deploy systemd unit file untuk aplikasi Node.js
      template:
        src: templates/nodejs-app.service.j2
        dest: "/etc/systemd/system/{{ app_name }}.service"
        owner: root
        group: root
        mode: '0644'
      notify: Reload systemd daemon dan restart nodejs app

    - name: Pastikan service diaktifkan saat boot dan dijalankan
      systemd:
        name: "{{ app_name }}"
        state: started
        enabled: true

    # Memaksa Ansible untuk menjalankan handler yang tertunda sekarang juga
    # agar kita bisa langsung memverifikasi port setelah restart terjadi
    - name: Terapkan perubahan konfigurasi tertunda (flush handlers)
      meta: flush_handlers

    - name: Verifikasi kesiapan port Node.js sebelum mengakhiri playbook
      wait_for:
        port: "{{ app_port }}"
        host: 127.0.0.1
        delay: 2
        timeout: 20
        state: started

  handlers:
    - name: Reload systemd daemon dan restart nodejs app
      systemd:
        name: "{{ app_name }}"
        state: restarted
        daemon_reload: true
        enabled: true

Dalam studi kasus ini, kita menggunakan fitur meta: flush_handlers. Fitur ini sangat penting karena secara default handler baru akan dieksekusi di akhir playbook. Dengan memanggil flush_handlers, kita memaksa Ansible untuk mengeksekusi proses reload daemon dan restart service saat itu juga. Setelah itu, task wait_for di bawahnya dapat memverifikasi port aplikasi yang baru saja dinyalakan ulang dengan aman dan akurat.


Ringkasan #

  • Modul Native systemd — Gunakan modul systemd alih-alih modul generik service pada distro modern untuk mendapatkan kontrol penuh atas siklus hidup service target.
  • Keuntungan Reload — Selalu utamakan opsi state: reloaded dibandingkan restarted untuk service yang mendukungnya guna mendapatkan update konfigurasi tanpa downtime.
  • Bahaya Restart Berulang — Ingat bahwa status restarted dan reloaded tidak idempoten; gunakan handler agar tindakan ini hanya dipicu sekali saat ada perubahan nyata.
  • Masking Layanan — Gunakan opsi masked: true untuk menonaktifkan service secara permanen agar tidak bisa dinyalakan oleh proses lain demi alasan keamanan.
  • Otomasi Daemon Reload — Pastikan parameter daemon_reload: true disertakan di task handler systemd setiap kali mendeploy atau mengubah systemd unit file (.service).
  • Verifikasi Kesiapan Port — Selalu gunakan modul wait_for setelah menyalakan service database atau API untuk memastikan port jaringan benar-benar terbuka sebelum task lain berjalan.
  • Polling HTTP Health Endpoint — Kombinasikan modul uri dengan parameter until untuk melakukan pengecekan kesehatan server web sebelum mengakhiri playbook deployment.
  • Inspeksi Status Aman — Gunakan changed_when: false dan daftarkan output status pemeriksaan (register) untuk membaca kondisi service tanpa memicu perubahan status playbook.

← Sebelumnya: Package   Berikutnya: Copy Module →

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