TEKNİK YAZI / Backend Sistemleri

systemd Nedir? Servis Birimi Nasıl Oluşturulur?

systemd ile Linux'ta servis birimi oluşturma: unit bölümleri, bağımlılık ile sıralama farkı, örnek .service dosyası, durum ve günlük inceleme adımları.

systemd, Linux sistemlerinde servisleri ve diğer sistem kaynaklarını “unit” adı verilen tanımlarla yöneten init ve servis yöneticisidir. Bir uygulamayı .service unit’i olarak tanımladığınızda systemd uygulamayı başlatabilir, durumunu izleyebilir, başarısız olduğunda yeniden çalıştırabilir ve günlüklerini journal üzerinden sunabilir.

systemd kullanan dağıtımlarda sistem yöneticisi PID 1 olarak çalışır. Servis yönetimi bunun yalnızca bir bölümüdür; unit türleri arasında şunlar bulunur:

Örneklerde /opt/my-app altındaki bir Python uygulaması my-app.service adıyla çalıştırılır. Uygulamayı /home altına koymak yerine /opt veya /srv kullanmak, ProtectHome= gibi sıkılaştırma seçenekleriyle çakışmayı önler.

Unit dosyasındaki bölümler

Bir servis unit’i genellikle [Unit], [Service] ve [Install] bölümlerinden oluşur. Direktiflerin hangi bölüme ait olduğu önemlidir.

[Unit]

systemd.unit kılavuzunda tanımlanan genel unit ilişkileri burada yer alır:

Önemli ayrım şudur: bağımlılık ve sıralama birbirinden ayrıdır. After=postgresql.service, PostgreSQL’i işlem kapsamına eklemez; yalnızca iki unit de başlatılacaksa bu servisin PostgreSQL’den sonra başlamasını söyler. PostgreSQL’in de etkinleştirilmesi gerekiyorsa Wants= veya gerçekten zorunluysa Requires= ilişkisi ayrıca yazılır.

Benzer biçimde Before=test.service, mevcut unit’in test.service’den önce çalışmasını ister; test.service’i önce başlatmak anlamına gelmez.

[Service]

Servise özgü çalışma ayarları systemd.service kılavuzunda tanımlanır:

User=, Group= ve ExecStart= genel [Unit] ayarları değildir; [Service] bölümüne yazılmalıdır.

[Install]

Bu bölüm, systemctl enable çalıştırıldığında unit’in hangi hedefe bağlanacağını belirtir. Uzun süre çalışan sistem servisleri için sıklıkla WantedBy=multi-user.target kullanılır.

Çalışan bir servis örneği

Aşağıdaki unit, sanal ortam içindeki Python yorumlayıcısını doğrudan çalıştırır:

[Unit]
Description=my-app uygulama servisi
Documentation=https://example.com/my-app/docs
Wants=network-online.target
After=network-online.target

[Service]
Type=exec
User=my-app
Group=my-app
WorkingDirectory=/opt/my-app
ExecStart=/opt/my-app/venv/bin/python -u /opt/my-app/app.py
EnvironmentFile=/etc/my-app.env
LoadCredential=db-password:/etc/my-app/db-password
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s

[Install]
WantedBy=multi-user.target

Bu örnek için my-app kullanıcısı, dizinler, sanal ortam, /etc/my-app.env ayar dosyası ve /etc/my-app/db-password dosyası önceden oluşturulmuş olmalıdır. Uygulamanın ağ tamamen hazır olmadan da başlayabilmesi durumunda network-online.target ilişkisine ihtiyacınız olmayabilir; gereksiz bağımlılıkları eklemeyin.

EnvironmentFile= port, ortam adı veya günlük düzeyi gibi ayarları taşımak için uygundur, sırlar için değil. systemd.exec kılavuzu ortam değişkenlerinin parola ve anahtar taşımaya uygun olmadığını açıkça belirtir: unit’e tanımlanan değişkenler D-Bus üzerinden yetkisiz istemcilere görünür ve süreç ağacı boyunca alt süreçlere aktarılır. Ayar dosyasını root:root sahipliğinde ve 0600 izinle tutmak yalnız dosyanın kendisini korur; değerler yine servisin ortamına kopyalanır.

Veritabanı parolası veya API anahtarı için LoadCredential= kullanın. systemd dosyayı servis başlarken okur, yalnız unit’in kullanıcısının (ve root’un) erişebildiği salt okunur bir dizine koyar ve bu dizinin yolunu $CREDENTIALS_DIRECTORY değişkeniyle bildirir. Sır dosyasını oluştururken değeri komut satırına yazmayın; aşağıdaki komut değeri standart girdiden okur, böylece kabuk geçmişine ve süreç listesine düşmez:

sudo install -d -m 700 -o root -g root /etc/my-app
# Parolayı yazın, ardından Enter ve Ctrl+D ile girişi bitirin
sudo sh -c 'umask 077 && cat > /etc/my-app/db-password'

Uygulama değeri ortamdan değil, bu dizindeki dosyadan okur:

import os
from pathlib import Path

credentials = Path(os.environ["CREDENTIALS_DIRECTORY"])
# cat ile girilen parolanın sonundaki tek satır sonunu kaldırır; parolanın
# kendi boşluklarına dokunmaz (Python 3.9+)
db_password = (credentials / "db-password").read_text().removesuffix("\n")

Sırrın diskte de şifreli durması gerekiyorsa systemd-creds encrypt ile şifreleyip LoadCredentialEncrypted= kullanabilirsiniz; ayrıntılar kılavuzda ve systemd credentials belgesinde anlatılıyor.

Type=exec, uzun çalışan basit servislerde systemd’nin kullanıcı veya yürütülebilir dosya gibi başlangıç hatalarını çağırana yansıtmasını sağlar. Kullanılan systemd sürümünde veya uygulamanın çalışma modelinde farklı bir Type= gerekebilir; özellikle fork eden daemon’lar ve hazır olma bildirimi yapan servisler için ilgili kılavuzu izleyin.

Durdurma sırasında systemd, varsayılan KillMode=control-group ayarıyla unit’in cgroup’undaki tüm süreçlere SIGTERM gönderir, TimeoutStopSec= kadar bekler ve hâlâ çalışan süreçleri SIGKILL ile sonlandırır. Bu yüzden örnekte yapay bir ExecStop=/bin/kill ... satırına gerek yoktur; sinyallerin farkı kill ve PID yazısında anlatılıyor. Varsayılan davranışı KillMode=process ile daraltmak çocuk süreçleri geride bırakabileceğinden genel bir şablon olarak kullanılmamalıdır.

Unit dosyasını güvenli biçimde oluşturma

Sabit ve açık bir dosya adıyla düzenleyiciyi açın:

sudoedit /etc/systemd/system/my-app.service

Unit içeriğini kaydettikten sonra sözdizimini kontrol edebilirsiniz:

sudo systemd-analyze verify /etc/systemd/system/my-app.service

Ardından systemd yöneticisinin dosyaları yeniden okumasını sağlayın ve servisi hem açılışa ekleyip hem de hemen başlatın:

sudo systemctl daemon-reload
sudo systemctl enable --now my-app.service

enable tek başına servisi hemen başlatmaz; yalnızca bir sonraki uygun açılış/target işlemi için bağlantıları oluşturur. systemctl üzerindeki --now seçeneği enable işlemiyle birlikte başlatmayı da ister.

Durum ve günlükleri inceleme

Servisin güncel durumunu görüntüleyin:

systemctl status my-app.service

Son 100 journal kaydını alın:

journalctl -u my-app.service -n 100 --no-pager

Yeni kayıtları canlı izlemek için:

journalctl -u my-app.service -f

journalctl filtresindeki -u, seçilen unit ve systemd tarafından o unit ile ilişkilendirilen iletileri gösterir. Yetki yapılandırmasına göre sistem günlüğünü okumak için sudo gerekebilir.

Unit dosyasını daha sonra değiştirirseniz yeni tanımı yüklemek ve çalışan servise uygulamak için:

sudo systemctl daemon-reload
sudo systemctl restart my-app.service

Önce systemctl status, ardından journalctl -u çıktısını kontrol ederek yeni sürecin sağlıklı başladığını doğrulayın.

Sarmalayıcı betik ne zaman gerekir?

Uygulamayı ExecStart= ile doğrudan çalıştırmak genellikle daha iyidir. Böylece systemd gerçek uygulama sürecini ana PID olarak takip eder; çıkış kodu ve sinyal davranışı araya gereksiz bir kabuk girmeden görünür.

Başlangıç öncesinde kabuğa özgü hazırlık adımları gerçekten gerekiyorsa bir sarmalayıcı betik (wrapper script) kullanılabilir. Betiğin son komutunda exec kullanmak, kabuk sürecini uygulamayla değiştirerek sinyallerin ve çıkış kodunun doğru sürece ulaşmasını kolaylaştırır:

#!/usr/bin/env bash
set -euo pipefail

# Yalnızca uygulamaya özgü zorunlu hazırlık adımları burada yapılır.
exec /opt/my-app/venv/bin/python -u /opt/my-app/app.py

Sanal ortamı source ile etkinleştirmek zorunlu değildir; sanal ortamın Python yürütülebilir dosyasını doğrudan çağırmak daha sade ve tekrarlanabilir bir unit üretir. Uygulama standart çıktıya ve standart hataya yazdığında systemd bunları varsayılan olarak journal’a aktarabilir.

Sonuç

Uzun süre çalışan bir uygulamayı systemd servisi olarak tanımlamak; açılışta başlatma, çökünce yeniden başlatma, düzenli durdurma ve merkezi günlük toplama işlerini tek bir unit dosyasına toplar. İyi bir unit, uygulamayı doğrudan ExecStart= ile çalıştırır, bağımlılığı (Wants=, Requires=) sıralamadan (After=) ayrı yazar, sırları ortam değişkeni yerine LoadCredential= ile verir ve her değişiklikten sonra systemd-analyze verify, systemctl status ve journalctl -u ile doğrulanır. Takvime bağlı işlerde aynı .service birimini bir .timer birimiyle eşleştirmek yeterlidir.

Birincil kaynaklar