Comprendere le Unità Systemd: Un'Analisi Approfondita della Configurazione dei Servizi

Scopri come funzionano le unità di servizio systemd, inclusi Unit, Service, Install, override, riavvii e log.

Comprendere le Unità Systemd: Un'Analisi Approfondita della Configurazione dei Servizi

I file di unità systemd sono piccoli file di testo che determinano come i servizi si avviano, da cosa dipendono, con quale utente vengono eseguiti e cosa succede in caso di fallimento. Se ti sei mai chiesto perché systemctl restart myapp.service funziona per un'app ma non per un'altra, la risposta di solito è nel file di unità.

Questa guida si concentra sulle unità .service perché sono quelle che amministratori e sviluppatori modificano più spesso. Lo stesso sistema gestisce anche socket, timer, mount, dispositivi, percorsi e target, ma i file di servizio sono dove si manifestano la maggior parte degli errori operativi.

Cosa sono i File di Unità Systemd?

I file di unità systemd sono semplici file di testo che contengono direttive di configurazione per una specifica unità. Un'unità rappresenta una risorsa gestita da systemd. Il tipo più comune è l'unità di servizio, che definisce come avviare, fermare, riavviare e gestire un processo in background o un'applicazione.

I file di unità sono organizzati in sezioni, ciascuna indicata da parentesi quadre ([]). Le sezioni più importanti per le unità di servizio sono:

  • [Unit]: Contiene metadati sull'unità, dipendenze e ordinamento.
  • [Service]: Definisce il comportamento del servizio stesso, inclusa la modalità di esecuzione.
  • [Install]: Specifica come l'unità deve essere abilitata o disabilitata, tipicamente collegandola a unità target.

Systemd cerca i file di unità in diverse directory standard, le più comuni sono:

  • /etc/systemd/system/: Per unità configurate localmente, che sovrascrivono quelle predefinite.
  • /usr/lib/systemd/system/: Per unità installate dai pacchetti su molte distribuzioni.
  • /lib/systemd/system/: Utilizzato da alcuni sistemi della famiglia Debian per unità fornite dai pacchetti.

Quando devi ispezionare un'unità, evita di indovinare il percorso. Usa:

systemctl cat nginx.service
systemctl show -p FragmentPath nginx.service

systemctl cat è particolarmente utile perché mostra l'unità base più eventuali override drop-in. Questa è la versione che systemd sta effettivamente utilizzando.

Anatomia di un File di Unità .service

Analizziamo un tipico file di unità .service per comprenderne i componenti.

La Sezione [Unit]

Questa sezione fornisce informazioni descrittive e definisce le relazioni tra le unità.

  • Description=: Una descrizione leggibile del servizio.
  • Documentation=: URL o percorsi alla documentazione del servizio.
  • After=: Specifica che questa unità deve avviarsi dopo che le unità elencate hanno terminato l'avvio.
  • Requires=: Simile a After=, ma rende anche le unità elencate obbligatorie. Se un'unità richiesta non si avvia, anche questa unità fallirà.
  • Wants=: Una forma più debole di dipendenza. Questa unità tenterà di avviare le unità desiderate, ma il loro fallimento non impedirà l'avvio di questa unità.
  • Conflicts=: Specifica le unità che non possono essere eseguite contemporaneamente a questa unità.

Esempio di sezione [Unit]:

[Unit]
Description=Il Mio Server Web Personalizzato
Documentation=https://example.com/docs/mio-server-web
After=network.target

Questo indica che il nostro server web personalizzato dovrebbe avviarsi dopo che la rete è disponibile.

Una trappola comune: After= controlla l'ordine, non il requisito. Se scrivi After=postgresql.service, systemd avvia il tuo servizio dopo PostgreSQL quando entrambi fanno parte della transazione, ma non tira automaticamente dentro PostgreSQL. Se la tua app ha veramente bisogno che PostgreSQL sia avviato dalla stessa transazione, usa anche Wants=postgresql.service o, per una dipendenza forte, Requires=postgresql.service.

Anche in questo caso, le dipendenze non sono controlli di salute. After=network.target non garantisce che DNS funzioni, che un'API remota sia raggiungibile o che un database accetti connessioni. La tua applicazione ha comunque bisogno di un comportamento di retry sensato.

La Sezione [Service]

Qui risiede la logica principale per l'esecuzione del servizio.

  • Type=: Definisce il tipo di avvio del processo. I tipi comuni includono:
    • simple (predefinito): Il processo principale è quello avviato da ExecStart=. Systemd considera il servizio avviato immediatamente dopo che il processo ExecStart= è stato forkato.
    • forking: Utilizzato per demoni tradizionali che fanno un fork di un processo figlio ed escono. Systemd attende che il processo padre esca.
    • oneshot: Per attività che eseguono un singolo comando e poi escono.
    • notify: Il servizio invia una notifica a systemd quando ha terminato l'avvio.
    • dbus: Per servizi che acquisiscono un nome D-Bus.
  • ExecStart=: Il comando da eseguire per avviare il servizio.
  • 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. Le opzioni includono no (predefinito), on-success, on-failure, on-abnormal, on-watchdog, on-abort e always.
  • RestartSec=: Il tempo di attesa prima di riavviare il servizio.
  • User= / Group=: L'utente e il gruppo sotto cui il servizio deve essere eseguito.
  • WorkingDirectory=: La directory di lavoro per i processi eseguiti.
  • Environment= / EnvironmentFile=: Imposta le variabili d'ambiente per il servizio.

Esempio di sezione [Service]:

[Service]
Type=simple
ExecStart=/usr/local/bin/mio-server-web --config /etc/mio-server-web.conf
User=www-data
Group=www-data
Restart=on-failure
RestartSec=5

Questa configurazione avvia il nostro server web, lo esegue come utente e gruppo www-data e lo riavvia automaticamente in caso di fallimento, con un ritardo di 5 secondi.

Type= merita attenzione extra. Molte unità rotte usano Type=forking perché un vecchio script init usava la modalità demone. Per un'applicazione moderna che rimane in primo piano, Type=simple è di solito corretto. Se il tuo processo fa un fork in background ma systemd non sa come identificare il vero processo principale, i report di stato e i riavvii possono diventare fuorvianti.

Per un lavoro una tantum, usa Type=oneshot e spesso RemainAfterExit=yes se l'azione completata dovrebbe contare come attiva. Ad esempio, un'unità che prepara una regola del firewall o monta una risorsa speciale può uscire con successo ma rappresentare comunque uno stato che ti interessa.

La Sezione [Install]

Questa sezione viene utilizzata quando si abilita o disabilita un'unità. Definisce come l'unità si integra con le unità target di systemd.

  • WantedBy=: Specifica il/i target che dovrebbero "volere" questa unità quando è abilitata. Per i servizi che dovrebbero avviarsi all'avvio, multi-user.target è comunemente usato.

Esempio di sezione [Install]:

[Install]
WantedBy=multi-user.target

Quando esegui systemctl enable my-custom-service.service, systemd crea un collegamento simbolico da /etc/systemd/system/multi-user.target.wants/ al tuo file di servizio, assicurando che si avvii quando il sistema raggiunge il runlevel multi-utente.

Se un'unità non ha una sezione [Install], può comunque essere perfettamente valida. Semplicemente non può essere abilitata direttamente con systemctl enable a meno che non esista un altro meccanismo di installazione. Alcune unità sono destinate a essere attirate da dipendenze, socket, timer o target invece di essere abilitate manualmente.

Creazione e Gestione di Unità di Servizio Personalizzate

Esaminiamo il processo di creazione di un'unità di servizio personalizzata.

Passo 1: Creare il File di Unità

Crea un nuovo file in /etc/systemd/system/ con estensione .service. Per il nostro esempio, creiamo /etc/systemd/system/my-app.service.

[Unit]
Description=Servizio Applicazione Personalizzata
After=network.target

[Service]
Type=simple
ExecStart=/opt/my-app/bin/run-app --port 8080
User=appuser
Group=appgroup
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Considerazioni Importanti:

  • Assicurati che il comando ExecStart punti a uno script o binario eseguibile accessibile e con permessi di esecuzione.
  • Crea l'User e il Group specificati se non esistono (sudo useradd -r -s /bin/false appuser, sudo groupadd appgroup, sudo usermod -a -G appgroup appuser).
  • Assicurati che l'applicazione possa essere avviata e fermata correttamente usando i comandi specificati.

Prima di inserire un comando in ExecStart=, eseguilo manualmente come stesso utente se possibile:

sudo -u appuser /opt/my-app/bin/run-app --port 8080

Questo cattura bit di esecuzione mancanti, directory mancanti, percorsi relativi errati e problemi di permessi prima che systemd sia coinvolto. Una volta che funziona manualmente, spostalo nell'unità e lascia che systemd gestisca la supervisione.

Passo 2: Ricaricare la Configurazione di Systemd

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

sudo systemctl daemon-reload

Questo comando scansiona i file di unità nuovi o modificati e aggiorna lo stato interno di systemd.

Passo 3: Abilitare e Avviare il Servizio

Per avviare immediatamente il servizio e configurarlo per l'avvio all'accensione:

sudo systemctl enable my-app.service  # Crea collegamenti simbolici per l'avvio
sudo systemctl start my-app.service   # Avvia il servizio ora

Passo 4: Gestire il Servizio

Usa i comandi systemctl per gestire il tuo servizio:

  • Controllare lo stato:

    sudo systemctl status my-app.service
    

    Questo mostrerà se il servizio è attivo, il suo ID processo, le voci di log recenti e altro.

  • Fermare il servizio:

    sudo systemctl stop my-app.service
    
  • Riavviare il servizio:

    sudo systemctl restart my-app.service
    
  • Ricaricare il servizio (se ExecReload= è definito):

    sudo systemctl reload my-app.service
    
  • Disabilitare il servizio (impedire l'avvio all'accensione):

    sudo systemctl disable my-app.service
    

Passo 5: Visualizzare i Log con journalctl

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

  • Visualizzare i log per un servizio specifico:

    sudo journalctl -u my-app.service
    
  • Seguire i log in tempo reale:

    sudo journalctl -f -u my-app.service
    
  • Visualizzare i log dall'ultimo avvio:

    sudo journalctl -b -u my-app.service
    

Migliori Pratiche e Suggerimenti

  • Usa Type=notify per applicazioni moderne: Se la tua applicazione lo supporta, Type=notify fornisce una migliore integrazione con systemd, permettendogli di tracciare accuratamente la prontezza del servizio.
  • Esegui i servizi come utenti non root: Specifica sempre User= e Group= nella sezione [Service] per minimizzare i rischi di sicurezza.
  • Definisci le dipendenze con attenzione: Usa After=, Requires= e Wants= per assicurarti che i servizi si avviino nell'ordine corretto e che le dipendenze critiche siano soddisfatte.
  • Sfrutta Restart=: Configura politiche di riavvio appropriate per garantire la disponibilità del servizio.
  • Mantieni semplici i file di unità: Per sequenze di avvio complesse, considera l'uso di script wrapper invocati da ExecStart= piuttosto che comandi complessi direttamente nel file di unità.
  • Usa systemctl cat <unità>: Per visualizzare il contenuto completo di un file di unità come lo vede systemd, inclusi eventuali override.
  • Usa systemctl edit <unità>: Questo comando apre un editor per creare un file di override per un'unità esistente, che è un modo più pulito per modificare i file di unità predefiniti rispetto a modificarli direttamente.

Modificare le Unità Esistenti in Modo Sicuro

Non modificare le unità di proprietà dei pacchetti in /usr/lib/systemd/system/ o /lib/systemd/system/ a meno che tu non stia eseguendo il debug su una macchina usa e getta. Gli aggiornamenti dei pacchetti possono sostituire quei file. Usa invece un override:

sudo systemctl edit nginx.service

Questo crea un drop-in in /etc/systemd/system/nginx.service.d/. Ad esempio, per aggiungere una politica di riavvio:

[Service]
Restart=on-failure
RestartSec=5s

Alcune direttive possono essere specificate più di una volta. Altre devono essere cancellate prima della sostituzione. ExecStart= è l'esempio classico:

[Service]
ExecStart=
ExecStart=/usr/local/bin/my-nginx-wrapper

La riga vuota ExecStart= resetta il valore precedente. Senza di essa, systemd potrebbe rifiutare l'unità o mantenere più comandi di quanto intendessi.

Dopo qualsiasi modifica all'unità o al drop-in, usa lo stesso ciclo di revisione:

sudo systemctl daemon-reload
systemctl cat my-app.service
sudo systemctl restart my-app.service
journalctl -u my-app.service -n 50 --no-pager

I file di unità non sono difficili una volta che separi i tre compiti: [Unit] descrive le relazioni, [Service] descrive il comportamento del processo e [Install] descrive l'abilitazione. La maggior parte del debug nel mondo reale consiste nel trovare quale di questi compiti è stato configurato con l'ipotesi sbagliata.

Un Esempio Realistico di File di Servizio

Ecco un piccolo ma realistico servizio per un'applicazione web Python:

[Unit]
Description=API Inventario
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=simple
User=inventory
Group=inventory
WorkingDirectory=/srv/inventory-api
EnvironmentFile=/etc/inventory-api/env
ExecStart=/srv/inventory-api/.venv/bin/gunicorn app:app --bind 127.0.0.1:9000
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

Ci sono diverse decisioni silenziose in quel file. Il servizio viene eseguito come inventory, non root. Il comando usa un percorso assoluto al gunicorn dell'ambiente virtuale, quindi non dipende dal PATH di una shell interattiva. L'app si lega a localhost perché un proxy inverso la esporrà pubblicamente. Il file di ambiente vive al di fuori dell'unità in modo che la distribuzione possa aggiornare la configurazione senza riscrivere i metadati del servizio di proprietà del pacchetto.

Le righe di dipendenza sono intenzionalmente modeste. After=postgresql.service controlla l'ordine se PostgreSQL fa parte della stessa transazione di avvio. Non prova che il database sia pronto per le connessioni e non sostituisce la logica di retry dell'applicazione. network-online.target può aiutare su sistemi che implementano correttamente la prontezza della rete, ma non è una garanzia universale che ogni dipendenza remota sia raggiungibile.

Se questo servizio fallisce, i primi controlli sono prevedibili:

systemctl status inventory-api.service
journalctl -u inventory-api.service -b --no-pager
systemctl cat inventory-api.service
sudo -u inventory /srv/inventory-api/.venv/bin/gunicorn app:app --bind 127.0.0.1:9000

L'ultimo comando non è qualcosa che lasci in esecuzione in produzione. È un controllo diagnostico che chiede: "L'utente configurato può eseguire questo comando?" Se non può importare l'app, leggere il file di ambiente o scrivere nella sua directory di log, systemd non lo risolverà per te.

Direttive di Risorsa e Sicurezza che Vedrai Spesso

Molte unità di produzione includono hardening o controlli delle risorse. Alcuni esempi comuni:

[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
MemoryMax=512M
CPUQuota=80%

Queste direttive possono essere molto utili, ma possono anche rompere le assunzioni. PrivateTmp=true dà al servizio un /tmp privato, quindi un altro processo potrebbe non vedere i file che scrive lì. ProtectHome=true può bloccare l'accesso a /home, /root e /run/user. ProtectSystem=full rende gran parte del sistema di sola lettura dal punto di vista del servizio. Se un'app improvvisamente non riesce a scrivere dove era abituata, ispeziona le impostazioni di hardening prima di incolpare l'applicazione.

I limiti delle risorse hanno lo stesso compromesso. MemoryMax= può impedire a un servizio di consumare l'intera macchina, ma se il valore è troppo basso, il servizio potrebbe essere ucciso sotto carico normale. Controlla il journal per messaggi di memoria esaurita e confronta il limite con l'uso reale prima di aumentarlo o rimuoverlo.

I Comandi di Debug Più Utili

Tienili a portata di mano quando lavori con le unità di servizio:

systemctl status my-app.service
systemctl cat my-app.service
systemctl show my-app.service
systemd-analyze verify /etc/systemd/system/my-app.service
journalctl -u my-app.service -b --no-pager

systemctl show è verboso, ma espone le proprietà che systemd ha calcolato dopo aver analizzato l'unità. Questo può rivelare un valore sorprendente ereditato da un'impostazione predefinita, un drop-in o una direttiva di reset. systemd-analyze verify cattura alcuni errori di sintassi e dipendenza prima di riavviare un servizio. Non sostituisce il test dell'applicazione, ma cattura abbastanza errori da valere la pena eseguirlo.