Własne usługi i timery
Moduł 6: Usługi i logi · Czas czytania: 11 min
Cel lekcji: Po tej lekcji będą Państwo potrafili uruchomić własny program jako usługę systemd pod osobnym użytkownikiem, z kilkoma opcjami bezpieczeństwa, oraz planować zadania za pomocą timera systemd lub crona.
Kluczowe pojęcia: plik unit, [Unit], [Service], [Install], ExecStart, User=, WorkingDirectory, EnvironmentFile, Restart=, WantedBy, NoNewPrivileges, ProtectSystem, ProtectHome, PrivateTmp, systemd-analyze security, timer, OnCalendar, Persistent, crontab
Dlaczego nie po prostu "nohup" albo "screen"?
Kto instaluje na serwerze własny program (małą aplikację webową, bota, skrypt, który co noc coś wykonuje), za pierwszym razem często uruchamia go ręcznie: node server.js & albo w sesji screen. Działa to do momentu, gdy się Państwo wylogują, serwer się zrestartuje albo program ulegnie awarii. Wtedy program znika i nikt tego nie zauważa.
Program, który ma działać zawsze, powinien być usługą systemd. Wtedy:
- uruchamia się sam przy starcie systemu;
- systemd uruchamia go ponownie, gdy ulegnie awarii;
- jego wyjście trafia do journala (lekcja 6.2);
- działa pod wybranym przez Państwa użytkownikiem, z uprawnieniami, na które Państwo pozwolą.
Własny plik unit
Załóżmy, że mają Państwo małą aplikację Node.js w /opt/factuurapp, która nasłuchuje na 127.0.0.1:3000. nginx lub Caddy udostępni ją później na zewnątrz (moduł 7).
Krok 1: osobny użytkownik bez powłoki (lekcja 3.2). Aplikacja nie musi działać jako root i nie musi się logować:
anna@web1:~$ sudo adduser --system --group --home /opt/factuurapp --no-create-home factuurapp
anna@web1:~$ sudo chown -R factuurapp:factuurapp /opt/factuurapp/dataSam kod może nadal należeć do roota (aplikacja nie musi modyfikować samej siebie); tylko katalog, do którego zapisuje, należy do użytkownika usługi.
Krok 2: plik unit. Własne unity umieszcza się w /etc/systemd/system/:
# /etc/systemd/system/factuurapp.service
[Unit]
Description=Factuurapp (Node.js)
After=network-online.target
Wants=network-online.target
[Service]
User=factuurapp
Group=factuurapp
WorkingDirectory=/opt/factuurapp
EnvironmentFile=/etc/factuurapp/env
ExecStart=/usr/bin/node /opt/factuurapp/server.js
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/opt/factuurapp/data
[Install]
WantedBy=multi-user.targetTrzy sekcje:
- [Unit]: opis i kolejność.
After=network-online.targetrazem zWants=sprawia, że usługa startuje dopiero wtedy, gdy sieć jest dostępna. - [Service]: jak działa program.
ExecStartz pełną ścieżką (systemd nie korzysta ze ścieżki wyszukiwania tak jak Państwa powłoka).User=iGroup=określają, pod kim działa program.EnvironmentFilewczytuje ustawienia, takie jak hasło do bazy danych, z osobnego pliku, aby nie znajdowały się w unicie. - [Install]: co robi
enable.WantedBy=multi-user.targetoznacza: uruchamiaj razem ze zwykłym startem systemu.
Krok 3: sekrety osobno, ze ścisłymi uprawnieniami.
anna@web1:~$ sudo mkdir /etc/factuurapp
anna@web1:~$ sudo install -m 600 -o root -g root /dev/null /etc/factuurapp/env
anna@web1:~$ sudoedit /etc/factuurapp/envZawartość to na przykład DATABASE_URL=postgres://factuur:...@127.0.0.1/factuur. systemd wczytuje ten plik jako root, zanim przełączy się na użytkownika usługi, więc 600 należący do roota w zupełności wystarczy.
Krok 4: wczytanie, uruchomienie, sprawdzenie.
anna@web1:~$ sudo systemctl daemon-reload
anna@web1:~$ sudo systemctl enable --now factuurapp
anna@web1:~$ systemctl status factuurapp
anna@web1:~$ journalctl -u factuurapp -fOpcje bezpieczeństwa: mały wysiłek, duża różnica
Cztery linie pod RestartSec w przykładzie ograniczają możliwości usługi, także wtedy, gdy ktoś znajdzie lukę w Państwa aplikacji:
| Opcja | Efekt |
|---|---|
NoNewPrivileges=true | proces i jego procesy potomne nigdy nie mogą uzyskać większych uprawnień (także przez sudo ani programy setuid) |
ProtectSystem=strict | cały system plików jest dla usługi tylko do odczytu, z wyjątkiem tego, na co Państwo pozwolą za pomocą ReadWritePaths= |
ProtectHome=true | /home, /root i /run/user są niewidoczne |
PrivateTmp=true | własny, pusty /tmp, którego inne programy nie widzą |
systemd potrafi ocenić Państwa unit:
anna@web1:~$ systemd-analyze security factuurapp
...
→ Overall exposure level for factuurapp.service: 4.9 OK 🙂Im niższa liczba, tym lepiej. Usługa bez żadnych opcji uzyskuje około 9 ("UNSAFE"). Nie trzeba dążyć do zera: każda dodana opcja może zablokować coś, czego aplikacja potrzebuje. Prosimy dodawać je pojedynczo i testować po każdym kroku. Jeśli coś przestanie działać, informację zwykle znajdą Państwo w journalu ("Read-only file system", "Permission denied").
Planowanie zadań: timery
Do czynności, które mają się odbywać o stałych porach (kopia zapasowa, skrypt porządkujący, raport), potrzebny jest timer. Timer systemd składa się z dwóch unitów o tej samej nazwie: .service, który mówi, co ma się wydarzyć, oraz .timer, który mówi, kiedy.
# /etc/systemd/system/opruimen.service
[Unit]
Description=Oude exportbestanden opruimen
[Service]
Type=oneshot
User=factuurapp
ExecStart=/usr/bin/find /opt/factuurapp/data/export -type f -mtime +30 -delete# /etc/systemd/system/opruimen.timer
[Unit]
Description=Dagelijks oude exportbestanden opruimen
[Timer]
OnCalendar=*-*-* 03:15:00
RandomizedDelaySec=10m
Persistent=true
[Install]
WantedBy=timers.targetType=oneshot oznacza: program się wykonuje i potem kończy działanie. OnCalendar to harmonogram: codziennie o 03:15. Można też wpisać daily, weekly lub Mon *-*-* 02:00; systemd-analyze calendar "Mon *-*-* 02:00" pokazuje, kiedy nastąpi kolejne uruchomienie. Persistent=true jest ważne: jeśli o 03:15 serwer był wyłączony, zadanie wykona się zaraz po starcie systemu. RandomizedDelaySec nieco rozprasza moment startu.
Włącza się timer, a nie usługę:
anna@web1:~$ sudo systemctl daemon-reload
anna@web1:~$ sudo systemctl enable --now opruimen.timer
anna@web1:~$ systemctl list-timers opruimen
NEXT LEFT LAST PASSED UNIT ACTIVATES
Tue 2026-11-10 03:19:42 CET 15h left - - opruimen.timer opruimen.service
anna@web1:~$ sudo systemctl start opruimen.service # teraz jednorazowo przetestuj ręcznie
anna@web1:~$ journalctl -u opruimen.serviceA cron?
cron to starsze, prostsze narzędzie, które nadal dobrze działa. Linia w crontabie użytkownika (crontab -e):
15 3 * * * /usr/local/bin/opruimen.shPięć pól to minuta, godzina, dzień miesiąca, miesiąc i dzień tygodnia. Zalety i wady obok siebie:
| cron | timer systemd | |
|---|---|---|
| Konfiguracja | jedna linia | dwa pliki |
| Pominięte uruchomienie (serwer wyłączony) | zostaje pominięte | z Persistent=true wykonuje się później |
| Wyjście i błędy | mailem, jeśli serwer potrafi wysyłać pocztę; w przeciwnym razie przepadają | w journalu |
| Opcje bezpieczeństwa, użytkownik | przez sam skrypt | wszystko z systemd (patrz wyżej) |
| Sprawdzenie, kiedy się wykonało | trudne | systemctl list-timers i journal |
Na serwerze zarządzanym na poważnie timery są lepszym wyborem, zwłaszcza dla kopii zapasowych (lekcja 8.2): chcą Państwo mieć pewność, że się wykonały i co zwróciły. Linię crona, która po cichu zawodzi, zauważą Państwo dopiero wtedy, gdy kopia zapasowa będzie potrzebna.
Na AlmaLinux i Rocky Linux
Unity i timery działają dokładnie tak samo. Jedna różnica: SELinux może zablokować własną usługę, która czyta lub zapisuje pliki w miejscu, którego SELinux się nie spodziewa, albo nasłuchuje na nietypowym porcie. Komunikat znajduje się w /var/log/audit/audit.log. Prosimy nie wyłączać SELinux, aby rozwiązać problem; należy znaleźć właściwą etykietę lub regułę (na przykład za pomocą semanage).
Podsumowanie
- Program, który ma działać zawsze, powinien być usługą systemd: sam się uruchamia, restartuje po awarii, a logi trafiają do journala.
- Własne unity w
/etc/systemd/system/, zUser=jako osobny użytkownik bez powłoki, sekrety wEnvironmentFilez uprawnieniami 600. NoNewPrivileges,ProtectSystem=strictzReadWritePaths,ProtectHomeiPrivateTmpograniczają szkody w razie luki;systemd-analyze securityocenia unit.- Timer to
.serviceplus.timerzOnCalendar;Persistent=truenadrabia pominięte uruchomienia. Włącza się timer, a nie usługę. - cron nadal działa, ale timery dają lepsze logi i nie pomijają uruchomień.
Quiz
Zadanie
Zadanie O6.3: Własna usługa z timerem. Proszę utworzyć na swoim serwerze małą własną usługę z timerem, na przykład skrypt, który co noc zapisuje wolne miejsce na dysku oraz systemctl --failed do pliku lub journala. Proszę użyć osobnego użytkownika, Type=oneshot, co najmniej dwóch opcji bezpieczeństwa oraz Persistent=true. Proszę przetestować za pomocą systemctl start, sprawdzić wyjście w journalu i zanotować wynik systemd-analyze security. Proszę wkleić swój .service oraz .timer i odpowiedzieć na pytania z listy kontrolnej.
Zadanie O6.3: Własna usługa z timerem
To zadanie wykonuje się samodzielnie, nikt go nie sprawdza. Prosimy zaznaczyć wykonane punkty i oznaczyć zadanie jako wykonane.
Dzięki Persistent=true zadanie zostanie wykonane po uruchomieniu systemu, jeśli wcześniej je pominięto. Wynik poniżej 6 jest dobry jak na pierwszą usługę; prawa zapisu tylko przez ReadWritePaths.
Zaznaczenia i notatki pozostają tylko w tej przeglądarce; nie są nigdzie wysyłane. Po zalogowaniu w Moje kursy zapisywana jest jedynie informacja, że zadanie zostało wykonane.
Przeczytaj więcej
- Instrukcja DirectAdmin 15.1: Zadania Cron: automatyczne uruchamianie zadań
- Podręcznik
systemd.exec(5)(User=, ProtectSystem=, NoNewPrivileges=): https://manpages.debian.org/trixie/systemd/systemd.exec.5.en.html - Podręcznik
systemd.timer(5)(OnCalendar=, Persistent=): https://manpages.debian.org/trixie/systemd/systemd.timer.5.en.html - Podręcznik
systemd.time(7)(harmonogramy): https://manpages.debian.org/trixie/systemd/systemd.time.7.en.html
