Contents

Пишем свой systemd unit

Юнит-файлы нужны для создания функциональности, которая необходима на сервере. Обычно под юнитом systemd понимается сервис, но на самом существует множество видов юнитов. Посмотрим полный список юнитов:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ systemctl -t help
Available unit types:
service
mount
swap
socket
target
device
automount
timer
path
slice
scope

Директории с юнитами:

  • /usr/lib/systemd/system: системные юнит-файлы. Они устанавливаются из пакетов вместе с nginx, mariadb и т.д.
  • /etc/systemd/system: юниты, созданные вручную, они затеняют системные юнит-файлы.
  • /run/systemd/system-runtime-юниты, которые были сгенерированны автоматически.

Пользовательские юнит файлы пользователя с именем username хранятся по аналогии в следующих директориях:

  • /usr/lib/systemd/username
  • /etc/systemd/username
  • /run/sustemd/username

Для примера создадим юнит для prometheus:

Посмотреть юнит-файл: systemctl cat prometheus Редактировать юнит-файл: sudo systemctl edit --full prometheus Откатить все изменения в юнит-файле: systemctl revert prometheus

Создадим юнит-файл $ sudo systemctl edit --force --full prometheus и добавим содержимое(ниже есть краткая версия):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# открываем секцию Unit
[Unit]

# Всё что указано здесь будет отображаться в service status
Description=Prometheus

# В systemd зависимости определяются правильным построением файлов юнитов. Например, юнит А требует, чтобы уже был запущен юнит B, для этого добавим строки Requires=B и After=B в раздел [Unit] юнит-файла A. Если зависимость является необязательной, укажите Wants=B и After=B соответственно. Обратите внимание, что Wants= и Requires= не подразумевают After=. Если After= не указать, то юниты будут запущены параллельно.
Wants=network.target
After=network.target

# Здесь указано всё о запускаемой программе
[Service]

# systemd запустит эту службу незамедлительно. Процесс при этом не должен разветвляться (fork). Если после данной службы должны запускаться другие, то следует использовать Type=forking.
Type=simple

# указываем пользователя и группу, от имени которых будет запускаться процесс:
User=prometheus
Group=prometheus

# указываем строку запуска приложения
ExecStart=/usr/local/bin/prometheus \
  --config.file /usr/local/etc/prometheus/prometheus.yml \
  --storage.tsdb.path /var/lib/prometheus/ \
  --storage.tsdb.retention.size "$VAL"

# указываем как делать reload сервиса
ExecReload=/bin/kill -HUP $MAINPID

# домашняя директория, отткуда будет запускаться бинарник, для прометеуса это не важно, тут для примера.
WorkingDirectory=/usr/local/etc/prometheus

# запущенное приложение должно отправлять сигнал что оно живо sd_notify(0, "WATCHDOG=1"), если в течении 10 секунд сигнала не будет, то система решит, что приложение зависло и перезапустит его.
WatchdogSec=10

# определяем перезапуск после сбоя
Restart=on-failure

# перед повторным запуском будет задержка, указанная здесь
RestartSec=10s

# Umask для процесса и его потомков. umask - инвертированный chmod.
UMask=0002
# ну и передадим переменную окружения VAL=26G
Environment=VAL=26G

# определяем, что директории /home, /root и /run/user будут недоступны и невидимы процессу
ProtectHome=true

# определяем что каталоги /usr, /boot, /efi и /etc будут доступны только read-only режиме
ProtectSystem=full

# определяем запуск сервиса на run lvl: multi-user runlevel
[Install]
WantedBy=multi-user.target
Run Lvl Target Units Description
0 runlevel0.target, poweroff.target Shut down and power off
1 runlevel1.target, rescue.target Set up a rescue shell
2,3,4 runlevel[234].target, multi-user.target Set up a non-gfx multi-user shell
5 runlevel5.target, graphical.target Set up a gfx multi-user shell
6 runlevel6.target, reboot.target Shut down and reboot the system

Type=oneshot удобен для сценариев, которые выполняют одно задание и завершаются. Если задать параметр RemainAfterExit=yes, то systemd будет считать процесс активным даже после его завершения.

Краткая версия:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[Unit]
Description=Prometheus
Wants=network.target
After=network.target

[Service]
Type=simple
User=prometheus
Group=prometheus
ExecStart=/usr/local/bin/prometheus \
  --config.file /usr/local/etc/prometheus/prometheus.yml \
  --storage.tsdb.path /var/lib/prometheus/ \
  --storage.tsdb.retention.size "$VAL"
ExecReload=/bin/kill -HUP $MAINPID
WorkingDirectory=/usr/local/etc/prometheus
WatchdogSec=10
Restart=on-failure
RestartSec=10s
UMask=0002
Environment=VAL=26GB
ProtectHome=true
ProtectSystem=full

[Install]
WantedBy=multi-user.target

Если юнит редактировался не с помощью systemctl edit --full prometheus, а вручную, нужно перечитать все юниты:

1
$ sudo systemctl daemon-reload

Теперь включим и запустим юнит prometheus"

1
2
3
4
$ sudo systemctl enable prometheus
$ sudo systemctl start prometheus
# одной командой
$ sudo systemctl enable --now prometheus

Команда systemctl edit prometheus создаст override-файл, в котором можно описать директивы, которые хотим добавить или изменить в существующем юните.

Чтобы посмотреть логи по юниту:

1
2
3
4
5
6
7
8
9
$ sudo journalctl --unit=prometheus
# с момента старта системы
$ sudo journalctl -b --unit=prometheus
# по PID
$ sudo journalctl _PID=4111
# c tail -f
$ sudo journalctl --unit=prometheus
# за последние 10 минут
$ sudo journalctl --unit=prometheus --since="10 min ago"

Пример создания юнита для монтирования. Так же добавим automount при обращении к директории.

Важное замечание: нужно сгенеровать имя юнита из пути, куда будет производиться монтирование, это можно сделать с помощью systemd-escape.

several man pages Per systemd.mount man page:

Where=

Takes an absolute path of a directory of the mount point. If the mount point does not exist at the time of mounting, it is created. This string must be reflected in the unit filename. (See above.) This option is mandatory.

The “see above” part is:

Mount units must be named after the mount point directories they control. Example: the mount point /home/lennart must be configured in a unit file home-lennart.mount. For details about the escaping logic used to convert a file system path to a unit name, see systemd.unit(5).

systemd-escape -p –suffix=mount “/home/goto/net_backups”

1
2
systemd-escape -p --suffix=mount "/home/goto/net_backups"
home-goto-net_backups.mount

На Centos 7 с помощью команды sudo systemctl edit --force --full home-goto-net_backups.mount не удалось создать юнит. Поэтому я создал его с помощью vi по пути /etc/systemd/system/home-goto-net_backups.mount и добавил следующее содержимое:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[Unit]
Description=CIFS
Requires=network-online.target
After=network-online.service

[Mount]
What=//samba.dmz.lan/accountants_folder/backup1c
Where=/home/goto/net_backups
Options=user,rw,credentials=/root/.smbclient,nounix,iocharset=utf8,file_mode=0777,dir_mode=0777,nofail
Type=cifs

[Install]
WantedBy=multi-user.target

Теперь перечитаем все юниты и запустим наш:

1
2
sudo systemctl daemon-reload
sudo systemctl start home-goto-net_backups.mount

Сейчас остановим home-goto-net_backups.mount и напишем ещё один юнит etc/systemd/system/media-nfs.automount для автомонтирования при обращении к целевой директории.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
sudo systemctl start home-goto-net_backups.mount
systemctl cat /etc/systemd/system/home-goto-net_backups.automount
# /etc/systemd/system/home-goto-net_backups.automount
[Unit]
Description=CIFS automount share
Requires=network-online.target
After=network-online.service

[Automount]
Where=/home/goto/net_backups
TimeoutIdleSec=301

[Install]
WantedBy=multi-user.target

Теперь остаётся включить и сразу запустить юнит. Это можно сделать одной командой:

1
sudo systemctl enable --now home-goto-net_backups.automount

Полезное: