Padroneggiare i file di servizio di Systemd: una guida completa

Crea file di servizio systemd affidabili con sezioni unità corrette, comportamento di riavvio, log, sicurezza e timer.

Padroneggiare i File di Servizio Systemd: Una Guida Completa

I file di servizio systemd dicono a Linux come avviare, fermare, riavviare e supervisionare la tua applicazione. Se il tuo servizio si avvia manualmente ma fallisce all'avvio, si riavvia in modo troppo aggressivo o scrive i log nella posizione sbagliata, di solito è il file unità che devi controllare.

Questa guida si concentra sulla creazione e configurazione dei file unità di servizio systemd da zero. Vedrai le sezioni principali, un esempio funzionante di servizio Python, comandi comuni per la risoluzione dei problemi e alcuni controlli di sicurezza e risorse utili in produzione.

Comprendere i File Unità Systemd

Systemd utilizza file unità per descrivere varie risorse di sistema come servizi, socket, dispositivi, punti di mount e altro. Un file unità di servizio, che tipicamente termina con l'estensione .service, definisce come systemd dovrebbe gestire un demone o un'applicazione specifica.

Questi file sono organizzati in sezioni, con ogni sezione contenente coppie chiave-valore che rappresentano direttive di configurazione. Le sezioni principali su cui ci concentreremo sono [Unit], [Service] e [Install].

Anatomia di un File di Servizio Systemd

Un tipico file di servizio systemd ha la seguente struttura:

[Unit]
Description=Una breve descrizione del servizio.
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/my_application --config /etc/my_app.conf
Restart=on-failure
User=myuser
Group=mygroup

[Install]
WantedBy=multi-user.target

Analizziamo ogni sezione e le sue direttive comuni:

La Sezione [Unit]

Questa sezione fornisce metadati sull'unità e definisce la sua relazione con altre unità. Viene utilizzata per dipendenze e ordinamento.

  • Description=: Un nome leggibile dall'uomo per il servizio. Questo è ciò che vedrai nell'output di systemctl status.
  • Documentation=: URL o percorsi alla documentazione per il servizio.
  • Requires=: Definisce dipendenze forti. Se un'unità elencata qui non riesce ad avviarsi, anche questa unità non riuscirà ad avviarsi.
  • Wants=: Definisce dipendenze deboli. Se un'unità elencata qui non riesce ad avviarsi, questa unità tenterà comunque di avviarsi.
  • Before=: Assicura che questa unità si avvii prima delle unità elencate.
  • After=: Controlla solo l'ordinamento. Ad esempio, After=network.target avvia questa unità dopo il target di rete di base, ma non garantisce la connettività esterna. I servizi dipendenti dalla rete potrebbero aver bisogno di After=network-online.target più il servizio wait-online della distribuzione.
  • Conflicts=: Se un'unità elencata qui viene avviata, questa unità verrà fermata e viceversa.

La Sezione [Service]

Questa sezione configura il comportamento del servizio stesso. È qui che definisci come avviare, fermare e gestire il processo.

  • Type=: Specifica il tipo di avvio del processo. I valori comuni includono:

    • simple (predefinito): Il processo principale è quello specificato in ExecStart=. Systemd presume che il servizio sia avviato immediatamente dopo che il processo ExecStart= è stato biforcato.
    • forking: Il processo ExecStart= biforca un figlio e il genitore esce. Systemd considera il servizio avviato quando il genitore esce. Spesso è necessario specificare PIDFile= con questo tipo.
    • oneshot: Simile a simple, ma si prevede che il processo esca dopo aver completato il suo lavoro. Utile per script di configurazione.
    • notify: Il demone invia un messaggio di notifica a systemd quando è stato avviato con successo. Questo è il tipo preferito per i demoni moderni che lo supportano.
    • dbus: Il servizio acquisisce un nome D-Bus.
  • ExecStart=: Il comando da eseguire per avviare il servizio. Per la maggior parte dei tipi di servizio, usa un comando ExecStart=. Più righe ExecStart= sono valide per Type=oneshot, dove vengono eseguite in sequenza.

  • ExecStop=: Il comando da eseguire per fermare il servizio.

  • ExecReload=: Il comando da eseguire per ricaricare la configurazione del servizio senza riavviarlo.

  • Restart=: Definisce quando il servizio deve essere riavviato automaticamente. Valori comuni:

    • no (predefinito): Non riavviare mai.
    • on-success: Riavvia solo se il servizio esce correttamente (codice di uscita 0).
    • on-failure: Riavvia se il servizio esce con un codice di uscita diverso da zero, viene terminato da un segnale o va in timeout.
    • on-abnormal: Riavvia se terminato da un segnale o va in timeout.
    • on-abort: Riavvia solo se terminato in modo non pulito da un segnale.
    • always: Riavvia sempre, indipendentemente dallo stato di uscita.
  • RestartSec=: Il tempo di attesa prima di riavviare il servizio (predefinito 100ms).

  • User=: L'utente con cui eseguire il servizio.

  • Group=: Il gruppo con cui eseguire il servizio.

  • WorkingDirectory=: La directory in cui spostarsi prima di eseguire i comandi.

  • Environment=: Imposta le variabili d'ambiente per il servizio.

  • EnvironmentFile=: Legge le variabili d'ambiente da un file.

  • PIDFile=: Percorso al file PID (spesso usato con Type=forking).

  • StandardOutput= / StandardError=: Controlla dove vanno stdout e stderr, come journal, null o inherit. Sulle distribuzioni comuni basate su systemd, l'output del servizio finisce normalmente nel journal a meno che le impostazioni predefinite del manager non siano state modificate.

La Sezione [Install]

Questa sezione definisce come l'unità deve essere abilitata o disabilitata, tipicamente creando collegamenti simbolici.

  • WantedBy=: Specifica il target che dovrebbe "volere" questo servizio quando è abilitato. Valori comuni:
    • multi-user.target: Per servizi che dovrebbero avviarsi quando il sistema raggiunge uno stato a riga di comando multiutente.
    • graphical.target: Per servizi che dovrebbero avviarsi quando il sistema raggiunge uno stato di login grafico.

Creare il Tuo Primo File di Servizio Systemd

Creiamo un semplice file di servizio per un ipotetico script Python chiamato my_app.py situato in /opt/my_app/my_app.py.

1. Crea il file di servizio:

I file di servizio per applicazioni personalizzate sono tipicamente collocati in /etc/systemd/system/. Chiamiamo il nostro file my_app.service.

# Crea la directory se non esiste
sudo mkdir -p /etc/systemd/system/

# Crea il file di servizio usando un editor di testo
sudo nano /etc/systemd/system/my_app.service

2. Aggiungi il seguente contenuto a my_app.service:

[Unit]
Description=La Mia Applicazione Python Personalizzata
After=network.target

[Service]
Type=simple
User=appuser
Group=appgroup
WorkingDirectory=/opt/my_app/
ExecStart=/usr/bin/python3 /opt/my_app/my_app.py
Restart=on-failure

[Install]
WantedBy=multi-user.target

Spiegazione dell'esempio:

  • Description: Identifica chiaramente la nostra applicazione.
  • After=network.target: Assicura che la rete sia disponibile prima di avviarsi.
  • Type=simple: Presume che my_app.py sia il processo principale e non biforca.
  • User=appuser, Group=appgroup: Specifica l'utente e il gruppo con cui l'applicazione deve essere eseguita. Assicurati che questi utenti e gruppi esistano sul tuo sistema e abbiano i permessi appropriati. Potresti doverli creare:
    sudo groupadd appgroup
    sudo useradd -r -g appgroup appuser
    sudo chown -R appuser:appgroup /opt/my_app/
    
  • WorkingDirectory: Imposta il contesto per lo script.
  • ExecStart: Il comando per eseguire lo script Python. Assicurati che /usr/bin/python3 sia il percorso corretto per il tuo interprete Python e che lo script sia eseguibile.
  • Restart=on-failure: Se lo script si blocca, systemd proverà a riavviarlo.
  • WantedBy=multi-user.target: Questo servizio verrà avviato automaticamente all'avvio del sistema in un ambiente multiutente.

3. Ricarica la configurazione del manager systemd:

Dopo aver creato o modificato un file di servizio, devi dire a systemd di ricaricare la sua configurazione.

sudo systemctl daemon-reload

4. Abilita e Avvia il Servizio:

  • Abilita: Questo fa sì che il servizio si avvii automaticamente all'avvio.
    sudo systemctl enable my_app.service
    
  • Avvia: Questo avvia il servizio immediatamente.
    sudo systemctl start my_app.service
    

5. Controlla lo Stato del Servizio:

Per verificare se il tuo servizio è in esecuzione e vedere eventuali errori:

sudo systemctl status my_app.service

Se ci sono problemi, il comando status mostrerà spesso messaggi di errore o log da journald.

6. Visualizzare i Log:

Systemd si integra con journald per la registrazione. Puoi visualizzare i log per il tuo servizio usando:

sudo journalctl -u my_app.service

Puoi anche seguire i log in tempo reale:

sudo journalctl -f -u my_app.service

Altri Comandi Utili

  • Ferma il servizio: sudo systemctl stop my_app.service
  • Riavvia il servizio: sudo systemctl restart my_app.service
  • Ricarica la configurazione (se supportato dall'app): sudo systemctl reload my_app.service
  • Disabilita l'avvio automatico all'avvio: sudo systemctl disable my_app.service

Configurazione Avanzata e Migliori Pratiche

Considerazioni sulla Sicurezza

  • Esegui i servizi come utenti non root: Specifica sempre User= e Group= a meno che non sia assolutamente necessario. Questo segue il principio del minimo privilegio.
  • Isola i servizi: Considera funzionalità di sandboxing come PrivateTmp=true, ProtectSystem=strict, ProtectHome=true e NoNewPrivileges=true. Testali con la tua applicazione perché possono bloccare scritture di file legittime.
    • PrivateTmp=true: Dà al servizio le proprie directory /tmp e /var/tmp private.
    • ProtectSystem=strict: Rende la maggior parte del filesystem in sola lettura per il servizio. Usa ReadWritePaths= per le directory in cui il servizio deve scrivere.
    • NoNewPrivileges=true: Impedisce al servizio di ottenere nuovi privilegi.

Gestire Avvii Complessi

  • Type=forking con PIDFile=: Per applicazioni più vecchie che biforcano, assicurati che PIDFile= punti al file corretto.
  • Type=notify: Se la tua applicazione lo supporta, questo è il modo più robusto per systemd di sapere quando è veramente pronta.
  • ExecStartPre= e ExecStartPost=: Comandi da eseguire prima e dopo ExecStart=. Utili per attività di configurazione o pulizia.

Controllo delle Risorse

Systemd ti permette di limitare l'uso delle risorse:

  • CPUWeight=: Peso relativo della CPU per il servizio.
  • MemoryMax=: Memoria massima che il servizio può utilizzare.
  • IOWeight=: Peso relativo I/O dove supportato dal kernel e dalla configurazione cgroup.

Esempio:

[Service]
# ... altre direttive ...
MemoryMax=512M
CPUWeight=50

Timer vs. Cron

I timer di systemd offrono un'alternativa moderna ai tradizionali lavori cron. Sono più flessibili e si integrano meglio con la gestione dei log e delle dipendenze di systemd.

  • Cron: Attività pianificate definite nei file crontab.
  • Timer Systemd (unità .timer): Queste unità pianificano unità .service. Definisci un file .timer che specifica quando deve essere eseguito un corrispondente file .service.

Esempio:

Per eseguire uno script ogni giorno alle 3:00:

  1. my_script.service: Il servizio da eseguire.

    [Unit]
    Description=Il mio script giornaliero
    
    [Service]
    Type=oneshot
    ExecStart=/opt/my_scripts/run_daily.sh
    User=scriptuser
    
  2. my_script.timer: Il timer che pianifica il servizio.

    [Unit]
    Description=Esegui il mio script giornaliero una volta al giorno
    
    [Timer]
    # Esegui alle 03:00 ogni giorno
    OnCalendar=*-*-* 03:00:00
    # Esegui subito dopo l'avvio se l'orario pianificato è stato perso mentre la macchina era spenta.
    Persistent=true
    
    [Install]
    WantedBy=timers.target
    

Per usarlo:

  • Metti entrambi i file in /etc/systemd/system/.
  • Esegui sudo systemctl daemon-reload.
  • Abilita e avvia il timer: sudo systemctl enable my_script.timer e sudo systemctl start my_script.timer.

I timer offrono vantaggi come Persistent=true (esegue i lavori persi all'avvio), eventi calendario (come hourly, daily, weekly) e una migliore integrazione con journalctl.

Risoluzione dei Problemi Comuni

  • Il servizio non si avvia: Controlla systemctl status <nome_servizio> e journalctl -u <nome_servizio>. Cerca errori di battitura, percorsi errati, dipendenze mancanti o errori di permesso.
  • Type= errato: Se un servizio fallisce immediatamente o si blocca, il Type= potrebbe essere sbagliato. Prova simple o forking e assicurati che PIDFile sia corretto se usi forking.
  • Permesso negato: Assicurati che User= e Group= specificati abbiano accesso in lettura/scrittura ai file e alle directory necessari.
  • Variabili d'ambiente: Se la tua applicazione si basa su variabili d'ambiente specifiche, assicurati che siano impostate correttamente usando Environment= o EnvironmentFile=.
  • Dipendenze: Verifica che After=, Wants= e Requires= corrispondano a ciò che intendi. After= ordina l'avvio; non tira un'altra unità da sola.

Prima di abilitare una nuova unità su un host di produzione, esegui:

sudo systemd-analyze verify /etc/systemd/system/my_app.service

Questo rileva molti errori di sintassi e direttive prima che tu faccia affidamento sul servizio all'avvio.

Punto Chiave

Scrivi il file di servizio più piccolo che descriva accuratamente la tua app, poi aggiungi deliberatamente politica di riavvio, logging, restrizioni di sicurezza e limiti di risorse. Dopo ogni modifica, esegui systemctl daemon-reload, verifica l'unità e controlla systemctl status più journalctl -u prima di fidartene in produzione.