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 halnyarestarted, 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 lewatsystemctl startmaupun secara otomatis karena dipicu oleh dependensi layanan lain. Ini sangat berguna jika kita ingin memastikan layanan bawaan yang berisiko (sepertitelnetatau 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:
- 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.
- Hanya Berjalan pada Status Changed: Handler hanya akan berjalan jika task yang memanggilnya mengembalikan status
changed. Jika task mengembalikan statusok(karena konfigurasi di server sudah sesuai dengan desired state), handler tidak akan dijalankan. Hal ini sangat penting untuk menjamin sifat idempoten. - Eksekusi Tunggal: Jika ada lima task berbeda yang memodifikasi konfigurasi Nginx dan semuanya memanggil handler
Restart Nginxyang sama, Ansible hanya akan menjalankan handler tersebut sebanyak satu kali di akhir play. Kita terhindar dari downtime berulang akibat restart yang tidak perlu. - 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.
Menunggu Kesiapan Layanan (Service Health Verification) #
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
systemdalih-alih modul generikservicepada distro modern untuk mendapatkan kontrol penuh atas siklus hidup service target.- Keuntungan Reload — Selalu utamakan opsi
state: reloadeddibandingkanrestarteduntuk service yang mendukungnya guna mendapatkan update konfigurasi tanpa downtime.- Bahaya Restart Berulang — Ingat bahwa status
restarteddanreloadedtidak idempoten; gunakan handler agar tindakan ini hanya dipicu sekali saat ada perubahan nyata.- Masking Layanan — Gunakan opsi
masked: trueuntuk menonaktifkan service secara permanen agar tidak bisa dinyalakan oleh proses lain demi alasan keamanan.- Otomasi Daemon Reload — Pastikan parameter
daemon_reload: truedisertakan di task handler systemd setiap kali mendeploy atau mengubah systemd unit file (.service).- Verifikasi Kesiapan Port — Selalu gunakan modul
wait_forsetelah menyalakan service database atau API untuk memastikan port jaringan benar-benar terbuka sebelum task lain berjalan.- Polling HTTP Health Endpoint — Kombinasikan modul
uridengan parameteruntiluntuk melakukan pengecekan kesehatan server web sebelum mengakhiri playbook deployment.- Inspeksi Status Aman — Gunakan
changed_when: falsedan daftarkan output status pemeriksaan (register) untuk membaca kondisi service tanpa memicu perubahan status playbook.