✻ Урок 1.8 · Тема 1: Linux, Bash и systemd
systemd и редакторы: «Заметки» как сервис
Содержание урока
Зачем это нужно
Никому не хочется ночью заходить на сервер, чтобы руками поднять упавший сервис. Для этого и существует systemd, и в этом уроке ты научишься на него полагаться.
Пока «Заметки» запущены руками в терминале (sudo -u notes python3 /opt/notes/app.py, урок 1.3), любое закрытое окно, перезагрузка сервера или падение процесса означают простой. Ночью никто не сидит перед терминалом и не нажимает Enter. На работе такое не допускается: сервис должен стартовать сам, подниматься после падения и писать логи в одно место, где их можно найти.
Всё это делает systemd (произносится «систем-ди»): программа, которая управляет запуском всех остальных программ на сервере. Ты будешь каждый день писать systemctl status (показать, жив ли сервис), systemctl restart (перезапустить) и journalctl -u (прочитать журнал сервиса), а конфиги придётся править прямо на сервере, в терминале. Графического интерфейса там нет, и единственные инструменты правки текста, которые под рукой, это nano и vim: текстовые редакторы, которые работают прямо в терминале, без мыши и окон (nano проще, vim есть почти на любом сервере). Поэтому в урок входят и они: без умения открыть, изменить и сохранить файл в редакторе все остальное не пригодится.
Шаг проекта: deploy/systemd/notes.service ставится в /etc/systemd/system/, конфиг выносится в /etc/notes/notes.env (права 640, root:notes), сервис работает под пользователем notes на 127.0.0.1:8080 и переживает kill -9 и перезагрузку. Версия app.py не меняется (v2.2).
Что нужно знать
- Урок 1.1: первый сервер: терминал,
apt, первое знакомство с редактором. - Урок 1.2: текст и конвейеры:
grep,wcи|нужны для фильтрации выводаjournalctl; там же объяснено, что такое stdout и stderr (два потока вывода программы). - Урок 1.3: пользователи и права: пользователь
notes, каталоги/var/lib/notesи/opt/notes, права 640 и 750,sudo. - Урок 1.4: процессы и сигналы: PID, SIGTERM и SIGKILL (systemd шлёт именно их), коды выхода (0 значит «всё хорошо»).
- Урок 1.5: диск, память, процессор: память процесса и OOM-killer (механизм, который убивает процесс при нехватке памяти), понадобятся для понимания лимитов сервиса.
- Урок 1.7: bash в эксплуатации: скрипты и cron, которые теперь будут ходить в сервис под systemd.
Сервис notes должен быть готов к установке: пользователь notes, /opt/notes/app.py версии v2.2 и каталог /var/lib/notes (создаются в уроке 1.3, версия кода приходит из уроков 1.4 и 1.5). Быстрая проверка того, что всё на месте, есть в начале практики.
Картина целиком
Представь большой офис, где работает много сотрудников. Кто-то должен открыть двери утром, посадить каждого на своё место, следить, чтобы никто не ушёл посреди рабочего дня, и вызвать замену, если человек заболел. Такой человек есть в офисе: администратор. Он не выполняет работу сам, он организует людей. Ещё он ведёт журнал происшествий, куда записывает всё, что случилось: кто пришёл, кто ушёл, что сломалось.
На Linux-сервере роль администратора играет systemd. «Сотрудники» это сервисы (service: программа, которая работает в фоне постоянно и ждёт запросов, например веб-сервер): notes, ssh, cron. Файл-«должностная инструкция» для каждого называется юнитом (unit). В нём написано, кто запускает сервис, какой командой, что делать, если он упал, и что ему разрешено трогать. Журнал происшествий это journal, а читают его командой journalctl.
flowchart TD
P["Включили питание"] --> K["Ядро Linux запускает PID 1 = systemd"]
K --> U["systemd читает юниты<br>/etc/systemd/system/ и /usr/lib/systemd/system/"]
U --> S1["ssh.service"]
U --> S2["notes.service<br>/usr/bin/python3 /opt/notes/app.py<br>от пользователя notes"]
U --> S3["cron.service"]
S2 --> R["Упал: перезапуск через 2 секунды<br>Restart=on-failure"]
S2 --> J["stderr уходит в journal"]
J --> JC["journalctl -u notes"]
E["Править файлы: nano или vim<br>системные безопаснее через sudoedit"] -.-> U
За урок ты разберёшь каждую стрелку: что такое юнит и как systemd его читает, как запускается процесс и что означают коды ошибок, как работает перезапуск и его предохранитель, как читать журнал, как ограничить права сервиса и как править файлы в vim и nano, не теряя рассудка.
Теория
Зачем нужен менеджер сервисов
Сервис (service, ещё говорят «демон», daemon) - это программа, которая должна работать постоянно и без участия человека: веб-сервер, база данных, наши «Заметки». В уроке 1.3 ты запускал «Заметки» вручную командой в терминале. У такого запуска есть проблемы, и каждая из них рано или поздно случается:
- Закрыл окно терминала, и программа умерла (ей отправили сигнал SIGHUP, урок 1.4).
- Сервер перезагрузился (обновление, авария питания), и программа не запустилась: некому нажать Enter.
- Программа упала из-за ошибки, и никто не заметил, пока не позвонили пользователи.
- Вывод программы уходит в окно терминала или в файл, который ты сам создал, и где он лежит, не знает никто, кроме тебя.
- Программа работает от твоего пользователя и может всё, что можешь ты.
Менеджер сервисов решает все пять проблем. В Linux он называется systemd, а в Ubuntu он стоит по умолчанию. Это не отдельная программа, которую ты запускаешь, а первый процесс системы: ядро (главная часть операционной системы) после загрузки запускает ровно одну программу, а всё остальное запускает уже она. Поэтому у systemd номер процесса (PID) равен 1, и это можно увидеть:
$ ps -p 1 -o pid,ppid,user,cmd
PID PPID USER CMD
1 0 root /sbin/init
ps показывает процессы (урок 1.4). Флаг -p 1 выбирает процесс с PID 1, -o pid,ppid,user,cmd задаёт колонки: номер, номер родителя, владелец и команда. У PID 1 родителя нет (PPID равен 0), это корень дерева процессов. /sbin/init это ссылка на systemd; на твоём сервере в колонке CMD может быть написано /usr/lib/systemd/systemd, это то же самое.
Аналогия: systemd это администратор офиса. Оговорка: обычный администратор не пускает в офис самого себя, а systemd сам запускается ядром первым и без него система не работает вообще. Именно поэтому его нельзя «выключить», как обычный сервис.
Прикинь сам: ты запустил «Заметки» руками командой в терминале и закрыл окно. Что будет с сервисом?
Он завершится: процесс привязан к терминалу и получит SIGHUP. Без менеджера сервисов некому поднять его заново после падения или перезагрузки.
Осторожно: Думают, что systemd это команда, которую вводят руками. Нет: команды для общения с ним называются systemctl («управлять systemd») и journalctl (читать журнал). systemd в их именах это просто «с кем разговариваем».
Главное: systemd запускает сервисы, перезапускает упавшие, ведёт журнал и ограничивает права; команды для общения с ним это
systemctlиjournalctl.
Проверь понимание: чем systemd лучше запуска через
nohup python3 app.py &из урока 1.4?
Ответ
nohup переживёт закрытие терминала, но не переживёт перезагрузку сервера и не поднимет программу после падения. Логи будут лежать там, куда ты сам их направил. systemd запускает сервис при загрузке, перезапускает по правилам, собирает вывод в единый журнал и умеет ограничить права. Кроме того, у него один интерфейс управления для всех сервисов сервера.
Менеджер есть. Теперь посмотрим, как ему объясняют, что именно запускать: это юнит.
Юнит: файл, в котором описан сервис
Юнит (unit) - это единица, которой управляет systemd: сервис, цель загрузки, таймер, точка монтирования диска. У каждого вида своё расширение: notes.service, multi-user.target, apt-daily.timer. Мы работаем только с .service (сервисами) и упоминаем .target (цели, объясним ниже). Юнит это текстовый файл с настройками, и никакого кода в нём нет.
Файлы юнитов лежат в двух местах, и различие важно:
/usr/lib/systemd/system/: юниты, которые принесли с собой пакеты (apt install nginxположит сюдаnginx.service). Их не правят: при обновлении пакета файл будет перезаписан. В Ubuntu 24.04 там 255 файлов./etc/systemd/system/: твои собственные юниты и переопределения. Если в обоих местах есть файл с одним именем, побеждает/etc. Обновления пакетов сюда не лезут.
Формат файла тот же, что у старых ini-файлов Windows: секции в квадратных скобках, а внутри строки Ключ=значение. Ключ - это имя настройки, значение - то, чему она равна. Строки, начинающиеся с #, это комментарии: systemd их пропускает.
В юните сервиса три секции:
[Unit]: описание и порядок запуска.Description=- подпись, которую увидишь вstatus.After=network.targetзначит «запускай меня после того, как поднята сеть».[Service]: как запускать. Пользователь, команда, правила перезапуска, ограничения.[Install]: что делать приenable(включении автозапуска): к какой цели (target) подключить сервис. Цель - это точка на пути загрузки: «сеть готова», «система дошла до рабочего состояния».multi-user.targetэто обычное рабочее состояние сервера без графики.WantedBy=multi-user.targetзначит «когда система доберётся до рабочего состояния, запусти и меня».
Вот весь юнит «Заметок», который ты напишешь в практике. Разберём его построчно (пояснения справа только для чтения, этот блок не копируй как есть, рабочий файл будет в практике):
[Unit]
Description=Сервис Заметки # подпись в systemctl status
After=network.target # стартовать после сети
[Service]
User=notes # от чьего имени работает процесс
Group=notes # и с какой группой
EnvironmentFile=/etc/notes/notes.env # файл с настройками для программы
ExecStart=/usr/bin/python3 /opt/notes/app.py # какую команду запустить
Restart=on-failure # поднять снова, если упала
RestartSec=2 # пауза перед перезапуском, секунды
ReadWritePaths=/var/lib/notes # сюда писать можно
ProtectSystem=strict # всё остальное только для чтения
NoNewPrivileges=true # нельзя получить больше прав
[Install]
WantedBy=multi-user.target # подключить к обычной загрузке
Комментарии справа здесь только для чтения урока: в настоящем юнит-файле # ставят в начале строки, а не после значения (иначе systemd прочтёт комментарий как часть значения). Про каждую из строк [Service] ниже отдельный раздел.
Главное правило про изменения. systemd читает файлы юнитов один раз и хранит их в памяти. Изменил файл на диске, а systemd об этом не знает, пока ты не скажешь: sudo systemctl daemon-reload («перечитай конфигурацию»). Аналогия: ты поправил рецепт в бумажной поваренной книге, а повар готовит по копии, которую переписал вчера. Пока не отдашь ему новую копию, он будет готовить по старой. Оговорка: повар не заметит расхождения, а systemd заметит и подскажет.
Вот что происходит на самом деле. Если поменять RestartSec=2 на 5 прямо в /etc/systemd/system/notes.service и не выполнить daemon-reload:
$ systemctl status notes --no-pager | head -2
Warning: The unit file, source configuration file or drop-ins of notes.service changed on disk. Run 'systemctl daemon-reload' to reload units.
● notes.service - Сервис Заметки
$ systemctl show notes -p RestartUSec
RestartUSec=2s
$ sudo systemctl daemon-reload
$ systemctl show notes -p RestartUSec
RestartUSec=5s
systemctl show notes -p RestartUSec печатает значение одной настройки так, как её сейчас знает systemd, а не как она записана в файле. До daemon-reload там было старое 2s, после стало 5s. Ещё status печатает предупреждение сверху: Warning: ... changed on disk. Это единственная защита от забывчивости.
Drop-in. Иногда нужно не переписать чужой юнит, а поправить пару строк. Для этого существует каталог имя.service.d/ с файлами *.conf внутри: systemd читает основной файл, потом все файлы из этого каталога, и строки из них дополняют или заменяют основные. Так делает команда sudo systemctl edit notes. Посмотреть итог со всеми дополнениями можно так: systemctl cat notes печатает все файлы, из которых сложен юнит, каждый под своим заголовком-комментарием с путём. Мы используем это в разделе «Сломай и почини».
Прикинь сам: ты поправил
RestartSec=2на 5 в/etc/systemd/system/notes.serviceи сделалsystemctl restart notes. Применится ли новое значение?
Нет: restart перезапускает процесс, а описание юнита systemd берёт то, что уже лежит в памяти. Сначала нужен systemctl daemon-reload, потом restart.
Осторожно: Думают, что restart перечитывает юнит-файл. Нет: restart перезапускает процесс, а описание юнита берёт то, что уже лежит в памяти. Для нового файла нужны оба действия: сначала daemon-reload, потом restart.
sequenceDiagram
participant Y as Ты
participant F as notes.service на диске
participant S as systemd
Y->>F: правка RestartSec=2 на 5
Note over S: в памяти по-прежнему 2
Y->>S: systemctl restart notes
S-->>Y: процесс перезапущен, но с RestartSec=2
Y->>S: systemctl daemon-reload
S->>F: перечитывает файл
Y->>S: systemctl restart notes
S-->>Y: теперь действует RestartSec=5
Главное: юнит это файл с описанием сервиса (
[Unit],[Service],[Install]); после правки файла выполняютdaemon-reload, потомrestart.
Проверь понимание: ты поправил
/etc/systemd/system/notes.service, выполнилsudo systemctl restart notes, а поведение не изменилось. Что забыл?
Ответ
sudo systemctl daemon-reload. Пока systemd не перечитал файл, он запускает процесс по старому описанию из памяти. systemctl status при этом пишет предупреждение changed on disk.
Файл описан. Теперь разберём, что именно systemd делает, когда запускает процесс.
Как systemd запускает процесс
Ключевая строка юнита это ExecStart=: полная команда запуска. Type= по умолчанию simple, и это значит: процесс, который стартует из ExecStart, и есть сервис, он остаётся на «переднем плане» и не уходит в фон. Раньше программы-демоны при старте сами делали fork (создавали копию себя и завершали исходный процесс), чтобы отвязаться от терминала. systemd такое не нужно: он сам держит процесс и сам следит за ним. Наши «Заметки» ведут себя именно так: запустились и работают.
Вывод программы systemd подключает к журналу. Python пишет логи в stderr, поэтому 2>> app.log из урока 1.2 больше не нужен: всё, что программа напишет в stdout или stderr, окажется в journal.
Пользователь и группа. User=notes и Group=notes значат, что процесс работает не от root, а от пользователя notes (из урока 1.3). systemd сам переключается на этого пользователя перед запуском команды. Поэтому в ExecStart не нужен sudo -u notes: sudo и есть то, что делал за тебя переключение, а теперь это делает systemd.
Разбор ошибок запуска по кодам. Когда сервис не стартовал, systemctl status показывает строку Process: ... (code=exited, status=NNN/ИМЯ). Число это код выхода (урок 1.4), и для запуска systemd придумал свои коды, которые начинаются с 200. По ним видно, на каком шаге остановился запуск:
| Код | Что значит | Типичная причина |
|---|---|---|
203/EXEC |
systemd не смог запустить команду из ExecStart |
путь неверный, файла нет, нет права на выполнение |
217/USER |
не удалось переключиться на пользователя из User= |
пользователя нет (опечатка или его не создали) |
2/INVALIDARGUMENT |
программа стартовала и сама завершилась с кодом 2 | ошибка в настройках, которую увидела программа |
Настоящий вывод при пользователе, которого нет:
Active: activating (auto-restart) (Result: exit-code) since Wed 2026-09-30 11:56:45 UTC; 539ms ago
Process: 1372 ExecStart=/usr/bin/python3 /opt/notes/app.py (code=exited, status=217/USER)
а в журнале причина расшифрована: notes.service: Failed to determine user credentials: No such process. То есть 203 и 217 это ошибки до запуска программы: сама программа даже не успела стартовать, значит смотреть нужно в юнит, а не в код приложения. Код 2, наоборот, приходит из программы: её собственная ошибка (например, PORT должен быть числом), и объяснение лежит в журнале строкой выше.
Путь к программе и отсутствие shell. В ExecStart лучше писать абсолютный путь: /usr/bin/python3, а не python3. Современный systemd умеет искать простое имя в своём фиксированном списке каталогов (/usr/local/bin, /usr/bin и других), не в твоём PATH, и на Ubuntu 24.04 запись ExecStart=python3 /opt/notes/app.py работает. Но такое поведение зависит от версии, и когда в системе несколько питонов, непонятно, какой запустится. Поэтому пиши полный путь и проверяй его командой command -v python3 (она печатает, где лежит программа).
Важнее другое: ExecStart это не shell. Символы >, |, && и $VAR bash понимает, а systemd нет. Если написать ExecStart=/usr/bin/python3 /opt/notes/app.py > /tmp/x.log, systemd просто передаст > и /tmp/x.log программе как обычные аргументы, а перенаправления не будет. Мы проверили это на стенде: сервис запустился, файл /tmp/x.log не появился. Если нужен shell, пишут ExecStart=/bin/bash -c '...', но лучше без него: вывод и так идёт в журнал.
Настройки программы: EnvironmentFile. «Заметки» читают свои настройки из переменных окружения (environment variables). Это пары «имя = значение», которые процесс получает вместе с запуском: HOST=127.0.0.1, PORT=8080. В коде на Python это строка os.environ.get("PORT", "8080"): «возьми переменную PORT, а если её нет, используй 8080». systemd умеет передать такие переменные из файла. EnvironmentFile=/etc/notes/notes.env значит: «прочитай этот файл и передай программе всё, что в нём написано».
Формат файла строгий: одна пара КЛЮЧ=значение на строке, без export и без пробелов вокруг =. Кавычки тоже не нужны. Наш файл:
HOST=127.0.0.1
PORT=8080
NOTES_DATA=/var/lib/notes/notes.txt
APP_VERSION=dev
Программа при старте видит: слушать на 127.0.0.1, порт 8080, хранить заметки в /var/lib/notes/notes.txt, версия называется dev.
Права на файл настроек. Мы задаём 640 и владельца root:notes. Расшифруем по правилам урока 1.3: владелец root читает и пишет (6), группа notes только читает (4), остальные не могут ничего (0). Сейчас в файле нет секретов, но в будущем появятся пароли, и лучше сразу привыкнуть к правильной модели.
Есть тонкость. Файл читает не сам процесс notes, а systemd, который работает от root. Поэтому сервис запустится, даже если у пользователя notes вообще нет права на чтение. Право группы notes нужно только для того, чтобы человек из группы мог посмотреть файл. Пользователь ubuntu без sudo файл не прочитает: cat: /etc/notes/notes.env: Permission denied, и это нормально.
Почему секреты нельзя класть в Environment=. В юните есть и ключ Environment=КЛЮЧ=значение, который задаёт переменную прямо в файле юнита. Но юнит-файлы не секретны: любой пользователь сервера может выполнить systemctl show notes -p Environment и увидеть значение. Проверено на стенде:
$ systemctl show notes -p Environment
Environment=DB_PASSWORD=hunter2
А EnvironmentFile показывает пользователю только путь к файлу (EnvironmentFiles=/etc/notes/notes.env (ignore_errors=no)), содержимое остаётся под правами 640. Поэтому пароли и токены кладут в файл с закрытыми правами, а не в юнит.
Прикинь сам: сервис в статусе
failed,status=203/EXEC. В какую сторону смотреть?
203 значит, что systemd не смог выполнить программу: неверный путь в ExecStart, нет бита исполнения или не найден интерпретатор. Саму программу смотреть рано.
Осторожно: Кажется, что User=notes даёт сервису права notes на всё, что читает systemd. Не так: systemd читает EnvironmentFile и юнит-файл как root, а процесс notes получает уже готовые значения. Права пользователя notes действуют только на то, что делает сама программа.
Главное: systemd запускает процесс от
User=, читаетEnvironmentFileкак root и передаёт программе готовые значения; коды вроде 203 это ошибка запуска, а не работы программы.
Проверь понимание: в юните написано
ExecStart=/usr/bin/python3 /opt/notes/app.py > /var/log/notes.log. Что произойдёт?
Ответ
Сервис запустится, но лог в файл не пойдёт: > это синтаксис bash, а systemd его не понимает и передаст > и /var/log/notes.log программе как два лишних аргумента. Вывод при этом всё равно окажется в journal. Если нужен именно файл, для этого в юните есть StandardOutput=append:/путь, но чаще журнала достаточно.
Процесс запущен. Что делает systemd, если тот упадёт?
Перезапуск и защита от бесконечных перезапусков
Ради этого ты и пришёл к systemd: сервис упал, и его нужно поднять без человека. За это отвечает Restart=. Слова в значении описывают, как завершился процесс:
Restart= |
Когда systemd поднимает процесс снова |
|---|---|
no |
никогда (значение по умолчанию) |
on-failure |
если код выхода не 0, процесс убит сигналом или завис (наш выбор) |
always |
всегда, даже после чистой остановки процесса с кодом 0 |
Штатная остановка перезапуска не вызывает. Когда ты сам выполняешь systemctl stop notes, systemd посылает процессу SIGTERM (урок 1.4). Наш app.py v2.1+ умеет корректно завершаться по SIGTERM и выходит с кодом 0. Для systemd это «остановили нарочно» и «завершился успешно»: перезапускать не нужно. Если процесс убили сигналом (kill -9 шлёт SIGKILL), это «упал»: пора вставать.
Разобранный пример, как это выглядит в журнале. Мы выполнили sudo kill -9 <PID>, вот что записал systemd (время и PID у тебя будут другими):
11:54:09 systemd[1]: notes.service: Main process exited, code=killed, status=9/KILL
11:54:09 systemd[1]: notes.service: Failed with result 'signal'.
11:54:11 systemd[1]: notes.service: Scheduled restart job, restart counter is at 1.
11:54:11 systemd[1]: Started notes.service - Сервис Заметки.
11:54:11 python3[727]: 2026-09-30 11:54:11,957 INFO started host=127.0.0.1 port=8080
Читаем сверху вниз. Первая строка: главный процесс завершился, code=killed (убит сигналом), status=9/KILL (сигнал номер 9, SIGKILL). Вторая: сервис закончил работу неудачей, причина «сигнал». Третья, через ровно две секунды (RestartSec=2): systemd планирует перезапуск, restart counter is at 1 считает, сколько раз он уже перезапускал. Четвёртая и пятая: запущен новый процесс с новым PID (был 717, стал 727), и программа сообщила started.
А после sudo systemctl stop notes в журнале другая картина: Stopping notes.service, потом строки самого приложения INFO shutting down и INFO stopped (из урока 1.4), затем Deactivated successfully. Перезапуска нет.
Предохранитель. Представь сервис, который падает сразу при старте. Если systemd будет бесконечно перезапускать его, получится цикл, который молотит процессор и забивает журнал. Поэтому у systemd есть ограничитель: если за StartLimitIntervalSec (по умолчанию 10 секунд) было больше StartLimitBurst (по умолчанию 5) запусков, он сдаётся. Юнит переходит в состояние failed, а в журнале пишет Start request repeated too quickly. После этого сам он больше не пытается, пока ты не выполнишь sudo systemctl reset-failed <юнит> (сбросить отметку «сломан», а счётчик попыток обнулить).
Настоящий вывод. Мы создали игрушечный сервис crashy.service с командой /bin/false (она сразу завершается с кодом 1) и RestartSec=1:
$ systemctl status crashy --no-pager
× crashy.service
Loaded: loaded (/etc/systemd/system/crashy.service; static)
Active: failed (Result: exit-code) since Wed 2026-09-30 11:57:09 UTC; 1s ago
Process: 1443 ExecStart=/bin/false (code=exited, status=1/FAILURE)
crashy.service: Scheduled restart job, restart counter is at 5.
crashy.service: Start request repeated too quickly.
crashy.service: Failed with result 'exit-code'.
Failed to start crashy.service.
Пять запусков за пять секунд, и systemd остановился.
Важная тонкость. У наших «Заметок» RestartSec=2: запуски идут раз в две-три секунды, и за 10 секунд набирается меньше пяти. Предохранитель не срабатывает, и сломанный сервис не переходит в failed, а годами висит в состоянии activating (auto-restart), то есть постоянно пытается запуститься. В сценарии 2 раздела «Сломай и почини» ты увидишь это своими глазами: счётчик restart counter дойдёт до десятков. Значит, отсутствие слова failed в status не доказывает, что сервис работает: смотри на Active: целиком.
Прикинь сам: ты убил сервис командой
kill -9, у негоRestart=on-failure. Что произойдёт? А послеsystemctl stop?
После kill -9 это сбой, и systemd поднимет сервис через RestartSec секунд с новым PID. После systemctl stop сервис остановлен намеренно, и перезапуска не будет.
Осторожно: Думают, что Restart=always «чинит» всё. Он лишь скрывает падения: сервис, который валится каждые две секунды, выглядит «почти живым». Причину нужно искать в журнале, а не ослаблять лимит.
flowchart TD
A["Процесс завершился"] --> B{"Как завершился?"}
B -->|"код 0 или systemctl stop"| C["Остановлен намеренно: без перезапуска"]
B -->|"код не 0 или сигнал"| D["Пауза RestartSec, перезапуск"]
D --> E{"Больше пяти запусков за 10 секунд?"}
E -->|"нет"| A
E -->|"да"| F["Предохранитель: failed, Start request repeated too quickly"]
Главное:
Restart=on-failureподнимает упавшее, а предохранитель (StartLimit) останавливает сервис, который падает слишком часто; причину ищут в журнале.
Проверь понимание: ты убил сервис командой
kill -9, у негоRestart=on-failure. Что произойдёт? А послеsystemctl stop?
Ответ
После kill -9 процесс завершён сигналом, это failure, и systemd поднимет его через RestartSec секунд (у процесса будет новый PID). После systemctl stop сервис остановлен намеренно, программа вышла с кодом 0, и он останется остановленным.
Перезапуск настроен. Теперь о двух разных вещах: запустить сейчас и запускать при загрузке.
Запустить сейчас и запускать при загрузке: start и enable
У сервиса два независимых свойства: работает ли он прямо сейчас и поднимется ли сам при загрузке сервера. Команды для них разные:
| Команда | Работает сейчас | Стартует при загрузке |
|---|---|---|
systemctl start notes |
да | не меняется |
systemctl stop notes |
нет | не меняется |
systemctl enable notes |
не меняется | да |
systemctl disable notes |
не меняется | нет |
systemctl enable --now notes |
да | да |
Аналогия: start это включить свет в комнате сейчас, а enable это поставить выключатель на автоматическое включение по расписанию. Можно сделать одно без другого. Оговорка: свет включается один раз, а enable действует при каждой загрузке.
Как работает enable, никакой магии: он создаёт символическую ссылку (symlink, файл-указатель на другой файл) в каталоге цели. При загрузке systemd доходит до multi-user.target и запускает всё, что лежит в его каталоге multi-user.target.wants/. Настоящий вывод:
$ sudo systemctl enable --now notes
Created symlink /etc/systemd/system/multi-user.target.wants/notes.service → /etc/systemd/system/notes.service.
Вот и вся суть: ссылка multi-user.target.wants/notes.service указывает на юнит. disable удаляет эту ссылку.
Как узнать текущее состояние? Полный systemctl status красивый, но неудобен в скриптах. Для скриптов есть короткие команды, которые печатают одно слово и возвращают код выхода (урок 1.4): systemctl is-active notes печатает active (код 0) или inactive/failed/activating (код не 0), systemctl is-enabled notes печатает enabled или disabled. Проверено: при остановленном сервисе is-active выводит inactive и возвращает код 3, поэтому в скриптах пишут if systemctl is-active --quiet notes; then ....
Ещё одна тонкость, на которой все спотыкаются. Команда systemctl start возвращается, как только процесс запущен, а не когда программа готова принимать запросы. Python стартует за десятки миллисекунд, и если сразу после enable --now выполнить curl, он может получить отказ, потому что порт ещё не открыт. Мы наблюдали это на стенде: curl сразу после старта закончился кодом 7 (Couldn't connect to server). Подожди секунду и повтори.
Прикинь сам: ты сделал
systemctl enable notesи не запускал сервис. Будет ли он работать сейчас? А после перезагрузки?
Сейчас нет: enable только включает автозапуск. После перезагрузки да, сервис стартует при загрузке.
Осторожно: enable не запускает сервис, а start не включает автозапуск. Если сделал только start, после перезагрузки сервиса не будет. Если сделал только enable, сервис заработает лишь при следующей загрузке.
flowchart LR
S["systemctl start notes"] --> N["Работает сейчас"]
S -.->|"после перезагрузки"| X["Сервиса нет"]
E["systemctl enable notes"] --> B["Запустится при загрузке"]
E -.->|"сейчас"| Y["Не запущен"]
EN["systemctl enable --now notes"] --> N
EN --> B
Главное:
startзапускает сейчас,enableвключает запуск при загрузке; нужны оба действия, аenable --nowделает их вместе.
Проверь понимание: ты выполнил
sudo systemctl enable notesи сразуcurl http://127.0.0.1:8080/healthz. Что получишь?
Ответ
Отказ соединения (Couldn't connect to server): enable только ставит ссылку для будущей загрузки, но не запускает процесс. Чтобы сделать и то и другое, нужен enable --now.
Сервис работает. Где посмотреть, что он делает? В журнале.
Журнал: journald и journalctl
До systemd каждая программа писала свой текстовый лог в свой файл: /var/log/nginx/error.log, /var/log/syslog. Чтобы разобраться в аварии, нужно было открывать пять файлов и сопоставлять время вручную. В systemd вывод всех сервисов собирает одна служба, journald, и складывает в единое хранилище, журнал (journal). Записи хранятся в бинарном формате, поэтому просто cat файла не поможет: читать их нужно командой journalctl. Зато каждая запись хранит метки: имя сервиса, время, PID, важность. По меткам можно искать быстро.
Как вывод программы попадает в журнал: процесс пишет в stdout или stderr, systemd подключил их к journald, тот превращает каждую строку в запись с метками. Мы ничего не настраиваем, всё работает само.
Разберём настоящую строку из журнала «Заметок»:
Sep 30 11:54:31 235dac9eaa5a python3[744]: 127.0.0.1 "GET /net-takoj-stranicy HTTP/1.1" 404 -
Sep 30 11:54:31- когда записано (по локальному времени сервера);235dac9eaa5a- имя машины (hostname), на твоём сервере другое;python3[744]- имя программы и её PID: по PID видно, какой именно запуск это написал;127.0.0.1 "GET /net-takoj-stranicy HTTP/1.1" 404 -- сообщение самого приложения: кто обратился, метод и путь запроса, код ответа (404 «не найдено»), а-значит, что размер ответа не указан.
Ходовые ключи journalctl:
| Ключ | Что делает |
|---|---|
-u notes |
только записи юнита notes (unit) |
-n 20 |
только последние 20 записей |
-f |
следить в реальном времени (follow), выход Ctrl+C |
--since "10 min ago" |
записи за последние 10 минут; работают yesterday, "2026-09-30 03:00" |
--until "..." |
до указанного момента |
-p warning |
только важность warning и выше (priority) |
-b |
только с текущей загрузки (boot); -b -1 - с предыдущей |
--no-pager |
не открывать less, печатать всё подряд (нужно в конвейере) |
-x |
добавить пояснения к сообщениям systemd (-xe листает к концу) |
Что такое pager. Когда вывод длинный, journalctl открывает его в программе-просмотрщике less: листаешь стрелками, выходишь клавишей q. В конвейере с grep или в скрипте это мешает, поэтому добавляют --no-pager.
Важность сообщений (priority) это число от 0 до 7: 0 emerg, 1 alert, 2 crit, 3 err, 4 warning, 5 notice, 6 info, 7 debug. -p err показывает записи важности 3 и выше, то есть 0-3. Есть подвох. Программа пишет в stderr, а journald ставит всем таким строкам важность 6 (info), даже если это ошибка приложения. Поэтому journalctl -u notes -p err не покажет ошибок самого приложения, только ошибки systemd (например, Failed to determine user credentials при отсутствующем пользователе). Мы проверили: на нашем стенде -p err вывел -- No entries --, хотя сервис падал и перезапускался, а -p warning показал строки Main process exited, code=killed. Для ошибок приложения лучше искать слова в тексте (grep) или по коду ответа.
Кто может читать журнал. Обычный пользователь видит только свои записи. Чтобы читать записи системных сервисов, нужно быть в группе adm или systemd-journal, либо запускать sudo journalctl. В Ubuntu первый пользователь, созданный при установке, уже в группе adm. Проверь: id. Если не в группе, journalctl напишет Hint: You are currently not seeing messages from other users and the system.
Журнал на диске и после перезагрузки. Записи не хранятся вечно: journald обрезает старые, когда журнал занимает слишком много места (по умолчанию примерно десятую часть диска, но не больше 4 ГБ). Сколько занимает сейчас: journalctl --disk-usage. Каталог /var/log/journal/ на Ubuntu существует, поэтому журнал переживает перезагрузку. В системах без этого каталога записи теряются при каждой перезагрузке, тогда нужно создать каталог или поставить Storage=persistent в /etc/systemd/journald.conf.
Прикинь сам: нужно понять, что случилось с сервисом вчера в 03:00. Какая команда?
journalctl -u notes --since "yesterday 02:55" --until "yesterday 03:10": журнал юнита за окно времени. В нём и вывод приложения, и записи самого systemd (Stopped, Scheduled restart job).
Осторожно: Думают, что journalctl -u notes показывает только логи приложения. Он показывает и записи самого systemd о юните (Started, Stopped, Scheduled restart job), и это удобно: причина падения и вывод приложения лежат рядом.
Главное:
journalctl -u имяпоказывает вывод сервиса вместе с записями systemd; сужают его по времени (--since), уровню (-p err) и свежим строкам (-f,-n).
Проверь понимание: как показать записи сервиса
notesза последний час, в которых встречается404?
Ответ
journalctl -u notes --since "1 hour ago" --no-pager | grep ' 404 '. Ключ -p err не подойдёт: приложение пишет 404 со строкой важности info, а не err.
Диагностика готова. Остался вопрос безопасности: как ограничить сервис, чтобы взлом одного не открыл сервер.
Песочница для сервиса: hardening
Сервис, слушающий сеть, это потенциальная цель. Если в приложении найдут уязвимость, атакующий получит права процесса. Чем меньше прав у процесса, тем меньше вреда. Для этого в systemd есть набор настроек, которые ограничивают процесс сильнее, чем обычные права Linux. Английское слово hardening (укрепление) обозначает именно это.
Аналогия: сотрудник склада работает в отдельной комнате, где ему выдан только нужный инструмент. Даже если он захочет пройти в кабинет директора, дверь просто закрыта. Оговорка: дверь закрывает systemd, и это не то же самое, что запрет на уровне пользователя, о котором мы говорили в уроке 1.3.
Два слоя защиты работают вместе:
flowchart TD
W1["notes пишет /var/lib/notes/notes.txt"] --> L1{"Слой 1: права Linux<br>notes владеет каталогом?"}
L1 -->|"да"| L2{"Слой 2: песочница<br>ReadWritePaths=/var/lib/notes?"}
L2 -->|"да"| OK["Запись разрешена"]
W2["notes пишет /etc/passwd"] --> M1{"Слой 1: права Linux<br>notes не root"}
M1 -->|"нет прав"| NO1["Отказ"]
M1 -.->|"даже root"| M2{"Слой 2: ProtectSystem=strict<br>/ только для чтения"}
M2 --> NO2["Отказ"]
Базовый набор для «Заметок»:
NoNewPrivileges=true: процесс и его потомки не могут получить больше прав, чем у них есть. Закрывает трюк с программами, которые запускаются от имени владельца (setuid, напримерsudo).ProtectSystem=strict: вся файловая система смонтирована для процесса только для чтения (кроме/dev,/proc,/sys). Он не может изменить/etc,/usr,/var.ReadWritePaths=/var/lib/notes: исключение из предыдущего правила. Сюда писать можно, потому что «Заметки» хранят тут данные.ProtectHome=true: каталоги/home,/rootи/run/userдля процесса пусты и недоступны.PrivateTmp=true: у сервиса свой/tmp, чужие файлы из общего/tmpон не видит, а его файлы не видят другие.
Все эти настройки работают не в приложении, а вокруг него: systemd строит для процесса особый вид файловой системы (для этого используется механизм ядра, называемый пространством имён, mount namespace), и приложению всё равно, что часть каталогов стала «невидимой» или «только для чтения».
Настоящий эксперимент, который показывает два слоя. Мы запустили одну команду touch (создать пустой файл) от пользователя notes тремя способами:
$ sudo -u notes touch /var/lib/notes/x
(файл создан: обычные права Linux пускают)
$ sudo systemd-run --quiet --wait --collect --pipe -p User=notes -p ProtectSystem=strict touch /var/lib/notes/x
/usr/bin/touch: cannot touch '/var/lib/notes/x': Read-only file system
$ sudo systemd-run --quiet --wait --collect --pipe -p User=notes -p ProtectSystem=strict -p ReadWritePaths=/var/lib/notes touch /var/lib/notes/x
(файл создан: исключение открыло каталог)
Read-only file system (файловая система только для чтения) это ошибка песочницы. Права Linux при этом в порядке: пользователю notes разрешено писать, но systemd закрыл запись сверху. Именно поэтому такую ошибку трудно искать: ls -l покажет, что права правильные.
Как оценить результат. Команда systemd-analyze security notes проверяет юнит по списку из десятков возможных ограничений и печатает итоговую оценку экспозиции: число от 0 (максимально закрыт) до 10 (максимально открыт) и слово: OK, MEDIUM, EXPOSED, UNSAFE. Гнаться за нулём не стоит: сервису, который слушает сеть и пишет на диск, часть возможностей нужна, и полностью закрыть его нельзя. Полезно другое: сравнивать оценку до и после правки и понимать, за что сняты баллы. На нашем юните оценка была 8.7 EXPOSED, а после добавления ProtectHome и PrivateTmp стала 8.3 EXPOSED. Проверка такая же, как любая проверка: число не самоцель, а способ увидеть, какие двери ещё открыты. У тебя число может отличаться: оно зависит от версии systemd.
Лимиты ресурсов. В юнит можно добавить и потолок по памяти (MemoryMax=256M) и по числу процессов (TasksMax=). systemd использует для этого cgroups (control groups): механизм ядра, который объединяет процессы сервиса в группу и считает, сколько ресурсов группа занимает. Если сервис превысил MemoryMax (и вытеснить память нельзя), ядро убивает процессы только этой группы, а не случайный процесс на сервере. Так утечка памяти в одном сервисе (вспомни /leak из урока 1.5) не роняет соседей. В нашем юните лимитов нет: мы их не проверяли на стенде и разбираем только идею.
Прикинь сам: сервис с
ProtectSystem=strictиReadWritePaths=/var/lib/notesпытается записать/etc/passwd. Что произойдёт?
Запись не пройдёт: / смонтирован только для чтения, исключение сделано лишь для /var/lib/notes. Это не сработает и у root, так как закрывает дверь сам systemd.
Осторожно: Думают, что ProtectSystem=strict защищает от всех атак. Он защищает от записи в системные каталоги, но не от кражи данных, которые сервис имеет право читать.
Главное: hardening это второй слой защиты поверх прав Linux:
ProtectSystem,ReadWritePaths,NoNewPrivilegesи лимиты памяти ограничивают то, что может сделать сам процесс.
Проверь понимание: сервису понадобилось писать логи в отдельный каталог
/var/log/notes. Что добавить в юнит?
Ответ
Добавить каталог в ReadWritePaths=: ReadWritePaths=/var/lib/notes /var/log/notes (значения через пробел), выполнить daemon-reload и restart. Без этого запись вернёт Read-only file system, даже если владелец каталога правильный. Убедись также, что каталог существует и принадлежит пользователю notes.
Юнит и защита описаны. Но файлы приходится править руками, и здесь нужны редакторы.
Редакторы vim и nano: править файлы в терминале
На сервере нет окна с мышью и курсором, есть только терминал (урок 1.1). Конфиги правят прямо в нём с помощью консольных редакторов (text editors). Два самых распространённых:
nano - простой. Открывается, ты печатаешь, внизу экрана подсказки. Знак ^ означает клавишу Ctrl, поэтому ^O это Ctrl+O. Настоящий экран nano выглядит так:
GNU nano 7.2 /var/tmp/notesCQWsdy7O.env
HOST=127.0.0.1
PORT=8080
NOTES_DATA=/var/lib/notes/notes.txt
APP_VERSION=dev
[ Read 4 lines ]
^G Help ^O Write Out ^W Where Is ^K Cut ^T Execute
^X Exit ^R Read File ^\ Replace ^U Paste ^J Justify
Сохранить: Ctrl+O, затем Enter (подтвердить имя файла). Выйти: Ctrl+X. Если вышел с несохранёнными правками, nano спросит Save modified buffer?: Y (сохранить), N (нет).
vim - мощный и непривычный. Он есть практически на любом сервере (в самых минимальных системах вместо него vi, его упрощённая версия), поэтому уметь им пользоваться нужно. Его особенность в том, что у него есть режимы (modes). Одни и те же клавиши делают разное в зависимости от режима:
i, a, o, A
+-----------------------------------------> +-------------------+
| обычный режим | режим вставки |
| (normal): клавиши это команды | (insert): клавиши |
| dd, p, u, /, : | вводят текст |
| <-----------------------------------------| |
+---------------------------------------- +-------------------+
Esc
: (двоеточие)
|
v
командная строка внизу: :wq :q! :%s/а/б/g , выполнить Enter
Почему так? Когда vim создавали, на клавиатурах не было привычных сегодня стрелок, а мыши не было тем более, поэтому все действия «повесили» на буквы, а вводить текст стали отдельным режимом. Аналогия: у радиотелефона две кнопки: «говорю» и «слушаю»; пока не нажал нужную, всё, что ты произносишь, слышит только комната. Оговорка: в vim переключатель «слушаю» включён по умолчанию, и ты сам должен нажать «говорю» (i).
Всегда начинаешь в обычном режиме. Из него i переводит в режим вставки (insert: текст вводится перед курсором), A (заглавная) в режим вставки в конце строки, а Esc возвращает обратно. Если ты не уверен, в каком режиме, нажми Esc пару раз: в обычном режиме это ничего не ломает.
Основные команды обычного режима:
| Клавиши | Действие |
|---|---|
i |
войти в режим вставки перед курсором |
A |
вставка в конце строки |
Esc |
вернуться в обычный режим |
dd |
вырезать строку (в буфер) |
yy |
скопировать строку (в буфер) |
p |
вставить из буфера после текущей строки |
u |
отменить последнее действие |
Ctrl+r |
повторить отменённое |
gg, G |
в начало файла, в конец |
/слово + Enter, n |
искать вперёд, перейти к следующему |
:w |
сохранить |
:wq или :x |
сохранить и выйти |
:q |
выйти (если нет несохранённых правок) |
:q! |
выйти без сохранения |
:%s/старое/новое/g |
заменить во всём файле |
Разбор двух непонятных вещей.
«Вырезать» и «буфер». dd не стирает строку навсегда: она удаляется из файла, но попадает в регистр (buffer, временное хранилище vim). Поэтому dd + p работает как «вырезать и вставить». Регистр хранит последнее вырезанное или скопированное. Поэтому, чтобы переместить строку в конец файла, делают dd, G, p.
Команда :%s/старое/новое/g. Разберём по частям: : вход в командную строку, % «во всём файле», s (substitute) «заменить», /старое/новое/ что на что, g (global) «во всех местах строки, а не только в первом». Мы проверили: :%s/строка/пункт/g на файле из пяти строк ответил 5 substitutions on 5 lines.
Разница между :wq и :x. :wq всегда записывает файл, :x записывает, только если были изменения. На практике разница видна в дате изменения файла: :wq обновит её, :x без правок оставит прежней. Проверено на стенде: после :x без изменений время файла не изменилось, после :wq изменилось.
Vim запоминает, где ты остановился. Если открыть тот же файл снова, курсор окажется там, где был при прошлом закрытии (эту память vim хранит в файле ~/.viminfo). Это удобно, но сбивает: dd на «первой» строке может удалить строку, которая на самом деле пятая. Перед правкой всегда смотри на номер строки в правом нижнем углу (4,1 значит «строка 4, столбец 1») и нажимай gg, если нужно начать с начала.
Правка системных файлов: sudoedit. Файлы вроде /etc/notes/notes.env принадлежат root. Обычная команда sudo vim файл запускает весь редактор от root, со всеми плагинами и возможностью открыть оболочку из редактора (:!bash): рискованно. Безопаснее sudoedit файл (то же, что sudo -e файл): редактор запускается от твоего пользователя над временной копией, а sudo подменяет оригинал только при сохранении. На стенде это выглядит так: nano открыл не /etc/notes/notes.env, а временный файл /var/tmp/notesCQWsdy7O.env, а после сохранения права остались root:notes 640.
Какой редактор откроет sudoedit, задаёт переменная окружения EDITOR (то же понятие, что переменные для программы выше, только для оболочки): export EDITOR=nano действует в этом терминале, чтобы сделать постоянным, добавь ту же строку в ~/.bashrc. Если EDITOR не задан, в Ubuntu откроется nano.
Что делать, если открыл системный файл без прав. Если открыть файл, на который нет прав записи, vim пустит тебя внутрь, но при сохранении :wq откажет: E45: 'readonly' option is set (add ! to override). Если ты уже набрал изменения, спасёт трюк :w !sudo tee %: :w !команда передаёт содержимое буфера на вход команды, sudo tee % записывает его в текущий файл (% это имя файла). Vim затем ругнётся W12: Warning: File ... has changed and the buffer was changed in Vim as well: это нормально, выйди :q!. Проверено на стенде.
Swap-файл. Пока ты редактируешь файл, vim держит рядом скрытый файл .файл.swp с копией правок. Если сеанс упал (оборвалась связь), файл остаётся, и при следующем открытии vim спросит:
E325: ATTENTION
Found a swap file by the name "~/.vim-drill.txt.swp"
owned by: ubuntu dated: Wed Sep 30 11:53:32 2026
file name: ~ubuntu/vim-drill.txt
modified: no
process ID: 609 (STILL RUNNING)
Смотри на process ID: если написано STILL RUNNING, файл сейчас открыт в другом окне, закрывай второе. Если процесса нет, сеанс упал, можно восстановить (R) или удалить swap-файл и открыть заново.
Прикинь сам: ты набираешь текст в vim, а он выполняет какие-то команды. Что случилось и как выйти без сохранения?
Ты в обычном режиме, а не в режиме вставки. Нажми Esc, затем :q! и Enter: выход без сохранения. Для вставки нужна i.
Осторожно: Главная паника новичка: «я печатаю, а vim выполняет какие-то команды». Причина всегда одна: он в обычном режиме, а не в режиме вставки. Нажми Esc, затем i.
Главное: в vim два режима: обычный (команды) и вставки (текст);
Escвозвращает в обычный,:wqсохраняет и выходит,:q!выходит без сохранения, а в nano подсказки внизу экрана.
Проверь понимание: ты открыл vim, нажал
i, набрал текст, потом нажалddи вместо удаления строки появилось словоdd. Почему?
Ответ
Ты остался в режиме вставки: в нём dd это просто две буквы. Нужно сначала нажать Esc (перейти в обычный режим), потом dd.
Редакторы освоены. Посмотрим, где эти знания встретятся дальше.
Где это встретится дальше
- Ты сам пишешь юниты и правишь их через
systemctl edit: в теме 2 сервисnginxполучит drop-in, аHOST=127.0.0.1изnotes.envпонадобится, когда перед «Заметками» встанет nginx. - Docker и Kubernetes (темы 4 и 5) делают ту же работу, что systemd: запускают, перезапускают, ограничивают ресурсы. Понятия те же:
Restartпревращается в политику перезапуска контейнера,ReadWritePathsв read-only файловую систему, лимиты cgroups вresources.limits. - Журнал превратится в централизованные логи (тема 8).
Прикинь сам: какие понятия systemd повторятся в Docker и Kubernetes?
Restart станет политикой перезапуска контейнера, ReadWritePaths read-only файловой системой, а лимиты cgroups resources.limits.
Главное: systemd, Docker и Kubernetes решают одну задачу (запустить, перезапустить, ограничить), поэтому знания переносятся.
Теория закончена. В практике ты напишешь unit-файл «Заметок», проверишь перезапуск и границы песочницы.
Практика
Все задания выполняются на твоём сервере (Ubuntu 26.04 или 24.04). Если ты на WSL2, systemd там нужно включить. Проверка: systemctl is-system-running. Если пишет offline, добавь в /etc/wsl.conf секцию [boot] со строкой systemd=true и выполни в PowerShell wsl --shutdown.
Перед началом проверь, что предыдущие уроки оставили всё на месте:
id notes
ls -l /opt/notes/app.py
stat -c '%a %U:%G %n' /var/lib/notes
command -v python3
id notes печатает данные пользователя notes, ls -l показывает файл app.py, stat -c '%a %U:%G %n' печатает права (%a), владельца и группу (%U:%G) и имя (%n) каталога данных, а command -v python3 покажет, где лежит интерпретатор.
uid=999(notes) gid=987(notes) groups=987(notes)
-rw-r--r-- 1 root root 5867 Sep 30 11:34 /opt/notes/app.py
750 notes:notes /var/lib/notes
/usr/bin/python3
Числа uid, gid и размер файла у тебя другие, важны сами строки: пользователь есть, app.py принадлежит root, каталог данных 750 notes:notes, а python3 лежит в /usr/bin/. Если id notes пишет no such user, вернись к уроку 1.3. Если у app.py не v2.2, скачай эталон: curl -fsSL -o ~/notes/app.py https://raw.githubusercontent.com/distinguished-sre/learning/main/devops/project/notes/versions/v2.2.py и скопируй в /opt/notes/ (sudo install -m 644 -o root -g root ~/notes/app.py /opt/notes/app.py).
Задание 1. Vim без паники
Цель: уверенно править файл в vim и выходить из него в любом состоянии.
Предскажи: ты открыл файл из пяти строк, нажал dd три раза, затем u. Сколько строк останется удалено? Что произойдёт, если набрать :q после изменения?
Ответ
u отменяет одно последнее удаление, значит, удалены две строки (первая и вторая), а третья вернулась. :q при несохранённых правках откажет: E37: No write since last change (add ! to override).
Шаги:
-
Подготовь тренировочный файл.
printfпечатает текст по шаблону:'строка %s\n'это шаблон (%sместо для подстановки,\nперевод строки), а1 2 3 4 5значения. Шаблон применяется к каждому значению по очереди, получается пять строк.>записывает вывод в файл (урок 1.2).printf 'строка %s\n' 1 2 3 4 5 > ~/vim-drill.txt cat ~/vim-drill.txt vim ~/vim-drill.txt -
Внутри vim нажимай по порядку (регистр букв важен; после каждой команды смотри на экран):
Что нажать Что произойдёт ddстрока 1 пропадает из файла (лежит в регистре) Gкурсор в конец файла pвырезанная строка вставлена после последней: 2, 3, 4, 5, 1 uвставка отменена: снова 2, 3, 4, 5 Ctrl+rвставка возвращена: 2, 3, 4, 5, 1 /строка 3, потомEnterкурсор прыгнул на «строка 3» A, затем ` (проверено)`ты в режиме вставки, в конце строки набран текст Escвернулся в обычный режим Команды с двоеточием и
/вводятся в командной строке внизу экрана. Пока набираешь/строка 3, текст виден внизу. -
Сохрани и выйди:
:wqиEnter. Проверь результат:cat ~/vim-drill.txt -
Испорти файл и выйди без сохранения. Открой файл снова:
vim ~/vim-drill.txt. Курсор окажется на той строке, где ты закончил (не на первой). Нажмиdd, затем:qиEnter: vim откажет. Теперь:q!иEnter. Убедись, что файл не изменился:cat ~/vim-drill.txt -
Необязательно. Открой файл ещё раз, набери
:%s/строка/пункт/gиEnter, посмотри, что получилось, и выйди без сохранения:q!.
Что должно получиться: после шага 3 и после шага 4 одно и то же.
строка 2
строка 3 (проверено)
строка 4
строка 5
строка 1
Как читать вывод:
- Строки идут не по порядку: «строка 1» переехала в конец (
dd,G,p), а к «строке 3» дописано слово из режима вставки. - Сообщение внизу vim после
:qпри несохранённых правках:E37: No write since last change (add ! to override). Словоadd !подсказывает выход::q!. - В правом нижнем углу vim показывает
строка,столбецкурсора:2,1значит вторая строка, первый символ. - После шага 5 внизу будет
5 substitutions on 5 lines: заменено пять вхождений в пяти строках.
Объясни себе: почему dd не удалил строку «навсегда»? В каком регистре она лежит до p? Чем :wq отличается от :x? Почему в шаге 4 dd оказался не на первой строке?
Типичные ошибки:
E37: No write since last change (add ! to override): пытаешься выйти:qс несохранёнными правками. Либо:wq, либо:q!.E45: 'readonly' option is set (add ! to override): файл без прав на запись. Выйди:q!и открой черезsudoedit.E325: ATTENTIONиFound a swap file by the name: файл уже открыт в другом vim или прошлый сеанс упал. Смотри строкуprocess ID: еслиSTILL RUNNING, закрой второй сеанс, иначе восстанови или удали.swp-файл.E486: Pattern not found: строка 3: ты искал текст, которого в файле нет. Проверь, что не удалил нужную строку.- Набираемый текст превращается в команды (
d,p,uделают что-то странное): ты в обычном режиме. НажмиEsc, затемi.
Пишешь unit-файл? Попроси нейросеть объяснить каждую строку и найти опасные настройки. Потом проверь
systemd-analyze verify /etc/systemd/system/notes.serviceи посмотриsystemctl cat notes.
Задание 2. Конфиг и unit-файл «Заметок»
Цель: создать конфиг /etc/notes/notes.env с нужными правами и unit-файл в каталоге проекта ~/notes.
Предскажи: какие права получит файл, созданный sudo tee, и сможет ли его прочитать пользователь notes без chgrp?
Ответ
По умолчанию 644 root:root (это umask 022 из урока 1.3): читать смогут все, а пользователь notes попадает в «остальные». Это неверная модель для конфига (в будущем в нём появятся секреты). Поэтому явно задаём 640 root:notes: читает только группа notes.
Шаги:
- Останови ручной запуск из урока 1.3, если он ещё работает (
Ctrl+Cв его терминале), иначе порт 8080 будет занят. -
Создай конфиг и права. Разбор команды:
sudo mkdir -p /etc/notesсоздаёт каталог (-pне ругается, если он уже есть).sudo tee /etc/notes/notes.env >/dev/nullзаписывает в файл то, что приходит на вход (teeчитает stdin и пишет в файл; нужен, потому чтоsudo cat > файлне сработал бы: перенаправление>выполняет твой shell, без прав root).>/dev/nullпрячет копию вывода, которуюteeпечатает на экран.<<'CFG'(heredoc) подаёт следующие строки до словаCFGкак ввод команды; кавычки вокругCFGотключают подстановку$внутри. Права:chown root:notes(владелец и группа) иchmod 640.sudo mkdir -p /etc/notes sudo tee /etc/notes/notes.env >/dev/null <<'CFG' HOST=127.0.0.1 PORT=8080 NOTES_DATA=/var/lib/notes/notes.txt APP_VERSION=dev CFG ls -l /etc/notes/notes.env sudo chown root:notes /etc/notes/notes.env sudo chmod 640 /etc/notes/notes.env ls -l /etc/notes/notes.env cat /etc/notes/notes.env -
В каталоге проекта
~/notesсоздай unit-файл в редакторе (vim или nano). Первая командаmkdir -pсоздаёт каталоги по всей цепочке.mkdir -p ~/notes/deploy/systemd vim ~/notes/deploy/systemd/notes.serviceОткрыв vim, нажми
i, вставь (или набери) содержимое файла целиком, затемEsc,:wq. Комментарии в файле на русском объясняют каждую группу строк:[Unit] Description=Сервис Заметки After=network.target [Service] User=notes Group=notes EnvironmentFile=/etc/notes/notes.env ExecStart=/usr/bin/python3 /opt/notes/app.py # Перезапуск после падения, пауза 2 секунды Restart=on-failure RestartSec=2 # Ограничения: файловая система только для чтения, писать можно в данные ReadWritePaths=/var/lib/notes ProtectSystem=strict NoNewPrivileges=true [Install] WantedBy=multi-user.targetПострочно:
Descriptionподпись,Afterпорядок запуска,UserиGroupучётная запись сервиса (безsudo -u),EnvironmentFileоткуда брать настройки,ExecStartкоманда с абсолютным путём,RestartиRestartSecправило перезапуска,ReadWritePathsиProtectSystemпесочница (запись только в/var/lib/notes),NoNewPrivilegesзапрет повышения прав,WantedByподключение к обычной загрузке. -
Проверь синтаксис до установки.
systemd-analyze verifyчитает файл юнита и сообщает о проблемах, ничего не запуская:systemd-analyze verify ~/notes/deploy/systemd/notes.service echo "код выхода: $?"
Что должно получиться:
-rw-r--r-- 1 root root 77 Sep 30 11:53 /etc/notes/notes.env
-rw-r----- 1 root notes 77 Sep 30 11:53 /etc/notes/notes.env
cat: /etc/notes/notes.env: Permission denied
код выхода: 0
Строк HOST=... в выводе нет: cat под обычным пользователем отказывает (это правильно). Прочитать файл можно через sudo cat /etc/notes/notes.env. Размер (77 байт), время и владельцы у тебя такие же или немного другие; важны права.
Как читать вывод:
-rw-r--r-- 1 root root: первыйlsдо правки, 644 иroot:root, как мы и предсказали.-rw-r----- 1 root notes: послеchownиchmodправа 640. Расшифровка:rw-владельцу (root),r--группеnotes,---остальным.Permission deniedотcat: ты не root и не в группеnotes. Так и задумано.systemd-analyze verifyпри исправном юните ничего не печатает, а$?(код выхода прошлой команды) равен 0. Любая строка вывода это замечание.
Объясни себе: зачем разделять notes.service (его потом положат в git, урок 3.1) и notes.env (только на сервере)? Почему ExecStart не содержит sudo -u notes? Почему cat отказал, а сервис файл прочитает?
Типичные ошибки:
-
Опечатка в имени ключа. Например,
ExecStarвместоExecStart. На Ubuntu 24.04verifyпишет:/tmp/t.service:9: Unknown key name 'ExecStar' in section 'Service', ignoring. t.service: Service has no ExecStart=, ExecStop=, or SuccessAction=. Refusing. Unit t.service has a bad unit file setting.На Ubuntu 26.04 первая строка чуть другая:
Unknown key 'ExecStar' in section [Service], ignoring.Смысл тот же: исправь ключ и повториverify. Assignment outside of section. Ignoring.: строки стоят до первого заголовка[...], потерян[Unit]в начале файла. Если потерян заголовок[Service]в середине, будет другое:Unknown key name 'User' in section 'Unit', ignoring.(systemd считает эти строки продолжением предыдущей секции).Command /usr/bin/python4 is not executable: No such file or directory: неверный путь вExecStart, проверьcommand -v python3.- Важно:
verifyне проверяет, существует ли пользователь изUser=и файлapp.py. Мы проверили: с несуществующим пользователем и с неверным именем скриптаverifyмолчит. Эти ошибки проявятся только при запуске. - В файле
notes.serviceпри вставке в vim съехали отступы (лесенка): в vim перед вставкой включи режим:set paste, или набирай текст руками.
Задание 3. Запуск, автозапуск, самовосстановление
Цель: установить сервис, проверить его состояние, убить и убедиться, что systemd его поднимает.
Предскажи: после kill -9 у сервиса изменится PID или нет? Что покажет systemctl status в строке Active:?
Ответ
PID станет другим: это новый процесс. Сервис будет active (running), но время в since свежее. В журнале появится строка про code=killed, status=9/KILL и Scheduled restart job.
Шаги:
-
Установи юнит и запусти.
sudo install -m 644копирует файл в нужное место с правами 644 (installэто копирование с заданием прав).daemon-reloadсообщает systemd о новом файле.enable --nowвключает автозапуск и запускает сервис сразу. Паузаsleep 1нужна из-за гонки:curlможет опередить открытие порта.sudo install -m 644 ~/notes/deploy/systemd/notes.service /etc/systemd/system/notes.service sudo systemctl daemon-reload sudo systemctl enable --now notes sleep 1 systemctl status notes --no-pager curl -s http://127.0.0.1:8080/healthz -
Убей процесс жёстко и проверь.
systemctl show notes -p MainPID --valueпечатает только номер главного процесса (без имениMainPID=).$(...)подставляет результат вkill, а-9это SIGKILL (урок 1.4).systemctl show notes -p MainPID sudo kill -9 "$(systemctl show notes -p MainPID --value)" sleep 3 systemctl show notes -p MainPID systemctl is-active notes -
Посмотри, что записал журнал, и проверь, что остановка не перезапускает:
sudo journalctl -u notes --no-pager | tail -n 6 sudo systemctl stop notes systemctl is-active notes sudo systemctl start notes systemctl is-active notes -
Проверь автозапуск перезагрузкой:
sudo reboot, после входаsystemctl is-active notesиcurl -s http://127.0.0.1:8080/healthz.
Что должно получиться: шаг 1.
Created symlink /etc/systemd/system/multi-user.target.wants/notes.service → /etc/systemd/system/notes.service.
● notes.service - Сервис Заметки
Loaded: loaded (/etc/systemd/system/notes.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-09-30 11:54:09 UTC; 1s ago
Main PID: 717 (python3)
Tasks: 1 (limit: 9519)
Memory: 9.8M (peak: 10.1M)
CPU: 37ms
CGroup: /docker/235dac9e.../system.slice/notes.service
└─717 /usr/bin/python3 /opt/notes/app.py
ok
Шаги 2 и 3:
MainPID=717
MainPID=727
active
Sep 30 11:54:09 235dac9eaa5a systemd[1]: notes.service: Main process exited, code=killed, status=9/KILL
Sep 30 11:54:09 235dac9eaa5a systemd[1]: notes.service: Failed with result 'signal'.
Sep 30 11:54:11 235dac9eaa5a systemd[1]: notes.service: Scheduled restart job, restart counter is at 1.
Sep 30 11:54:11 235dac9eaa5a systemd[1]: Started notes.service - Сервис Заметки.
Sep 30 11:54:11 235dac9eaa5a python3[727]: 2026-09-30 11:54:11,957 INFO started host=127.0.0.1 port=8080
inactive
active
PID, время, имя хоста и размер памяти у тебя будут другими. Важно: второй MainPID отличается от первого, а после stop статус inactive. Строка CGroup: у тебя будет короче, без слова docker: оно у нас потому, что стенд запущен в контейнере. После reboot is-active печатает active, а curl возвращает ok.
Как читать вывод:
Loaded: ... enabled; preset: enabled: юнит прочитан из/etc/systemd/system/notes.service, автозапуск включён.presetэто рекомендация дистрибутива, сейчас не важна.Active: active (running) since ...; 1s ago: сервис работает, время старта. Если послеkill -9смотретьsince, время свежее.Main PID: 717 (python3): номер и имя главного процесса.Tasks,Memory,CPU: сколько процессов, памяти и процессорного времени использует группа сервиса (это данные cgroup).CGroupи дерево: какие процессы входят в сервис. У нас один:/usr/bin/python3 /opt/notes/app.py.okэто ответ/healthz.Created symlink ... →подтверждает, чтоenableсоздал ссылку.- Журнал: сначала
killed, status=9/KILL, ровно через две секундыScheduled restart job, затем старт нового процесса с новым PID. inactiveпослеstopи сноваactiveпослеstart: остановка не вызвала перезапуск.
Объясни себе: почему после stop сервис не поднялся, а после kill -9 поднялся? Чем is-active удобнее status в скрипте?
Типичные ошибки:
curl: (7) Failed to connect ... Couldn't connect to serverсразу послеenable --now: процесс ещё не открыл порт. Подожди секунду и повтори. (Ubuntu 26.04 напишетCould not connect to server: смысл тот же.)OSError: [Errno 98] Address already in useв журнале: порт 8080 занят ручным запуском из урока 1.3. Найди:sudo ss -ltnp | grep 8080и останови процесс (ssразберём в уроке 2.1).Failed to enable unit: Unit notes.service does not exist.(в 26.04 может бытьUnit file notes.service does not exist.): файл не в/etc/systemd/system/или имя неверное.System has not been booted with systemd as init system (PID 1). Can't operate.: это WSL2 без systemd, см. начало практики.Job for notes.service failed because the control process exited with error code.: сервис не стартовал. Причина вsystemctl status notesиjournalctl -xeu notes.
Не понимаешь строку из
journalctl? Дай нейросети 20 строк вокруг события и спроси, что произошло и в каком порядке. Потом проверь время и PID вsystemctl statusиjournalctl -u notes -o verbose.
Задание 4. Журнал: читать и искать
Цель: научиться доставать из журнала нужное за секунды.
Предскажи: сколько строк на один запрос curl /healthz появится в журнале? Попадёт ли туда что-то из /healthz?
Ответ
Ни одной. Код app.py v2.2 не пишет в лог запросы на служебные пути /healthz и /readyz (иначе журнал заполнялся бы проверками), поэтому запрос /healthz строки не даст, а запрос /notes даст.
Шаги:
-
Сгенерируй события и посмотри последние записи.
curl -sне печатает полосу загрузки,>/dev/nullвыбрасывает ответ: нам нужен только след в журнале.curl -s http://127.0.0.1:8080/healthz curl -s http://127.0.0.1:8080/notes >/dev/null curl -s http://127.0.0.1:8080/net-takoj-stranicy >/dev/null journalctl -u notes -n 10 --no-pager -
Отфильтруй. Разбор:
grep ' 404 'оставляет строки с кодом ответа 404 (пробелы вокруг важны, иначе поймаешь и другие числа),wc -lсчитает строки (урок 1.2),-p warningоставляет важность warning и выше.journalctl -u notes --since "5 min ago" --no-pager | grep ' 404 ' journalctl -u notes -b --no-pager | wc -l journalctl -u notes -p warning --no-pager journalctl -u notes -p err --no-pager -
Посмотри, сколько занимает журнал, и какие юниты есть:
journalctl --disk-usage systemctl list-units 'notes*' --no-pager -
Открой второе окно терминала, выполни там
journalctl -u notes -f, а в первом окне сделайcurl -s http://127.0.0.1:8080/notes >/dev/null. Новая строка появится во втором окне сразу. Выйди из-fпоCtrl+C.
Что должно получиться: шаг 1 (у тебя выше будут строки про старт, остановку и перезапуск из задания 3).
Sep 30 11:54:12 235dac9eaa5a python3[744]: 2026-09-30 11:54:12,998 INFO started host=127.0.0.1 port=8080
Sep 30 11:54:31 235dac9eaa5a python3[744]: 127.0.0.1 "GET /notes HTTP/1.1" 200 -
Sep 30 11:54:31 235dac9eaa5a python3[744]: 127.0.0.1 "GET /net-takoj-stranicy HTTP/1.1" 404 -
Шаги 2 и 3:
Sep 30 11:54:31 235dac9eaa5a python3[744]: 127.0.0.1 "GET /net-takoj-stranicy HTTP/1.1" 404 -
15
Sep 30 11:54:09 235dac9eaa5a systemd[1]: notes.service: Main process exited, code=killed, status=9/KILL
Sep 30 11:54:09 235dac9eaa5a systemd[1]: notes.service: Failed with result 'signal'.
-- No entries --
Archived and active journals take up 16.0M in the file system.
UNIT LOAD ACTIVE SUB DESCRIPTION
notes.service loaded active running Сервис Заметки
Имя хоста, время, PID, число строк (15), размер журнала у тебя отличаются.
Как читать вывод:
- Каждая строка журнала: время, хост,
программа[PID], сообщение. Значение колонок разобрано в теории. - Запроса
/healthzв выводе нет: приложение его не логирует. - Строка с
404: приложение записало, что отдало 404 на несуществующий путь. wc -lсчитает записи текущей загрузки для юнита. Число растёт с каждым запросом.-p warningпоказал только записи systemd о том, что процесс был убит: это предупреждения.-p errпуст: приложение пишет всё в info, а ошибок уровня err нет. Значит, пустой вывод не означает, что проблем не было.journalctl --disk-usageпоказывает, сколько места занимает журнал: смотри на него, когда на диске заканчивается место (урок 1.5).LOAD active running: юнит загружен, активен, процесс работает.
Объясни себе: зачем --no-pager в конвейерах? Почему -p err ничего не нашёл, хотя сервис падал? Почему журнал важно контролировать после урока 1.5?
Типичные ошибки:
-- No entries --: неверное имя юнита (notez) или за выбранное время записей нет. Проверьsystemctl list-units 'notes*'и ключ--since.Hint: You are currently not seeing messages from other users and the system.: пользователь не в группеadmилиsystemd-journal. Используйsudo journalctlили добавь себя:sudo usermod -aG systemd-journal $USERи зайди заново.journalctlоткрылlessи «завис»: это pager. Выйди клавишейqи добавь--no-pager.grep '404'находит лишнее (например, время11:54:04): ищи с пробелами вокруг кода,' 404 '.
Задание 5. Шаг проекта: hardening и проверка границ
Цель: убедиться, что ограничения из unit-файла действительно работают, и зафиксировать состояние проекта.
Предскажи: сможет ли сервис после ProtectSystem=strict записывать заметки в /var/lib/notes/notes.txt? А если убрать ReadWritePaths?
Ответ
С ReadWritePaths=/var/lib/notes сможет. Без него запись даст Read-only file system, POST /notes вернёт 500 ({"error": "storage"}), а /readyz станет 503 (not ready): приложение проверяет, можно ли писать в каталог данных.
Шаги:
-
Запиши заметку и убедись, что она лежит в файле данных.
-X POSTзадаёт метод,-dтело запроса (JSON, урок 1.1).sudo tail -n 1печатает последнюю строку файла: нуженsudo, потому что каталог/var/lib/notesзакрыт правами 750.curl -s -X POST http://127.0.0.1:8080/notes -d '{"text":"Заметка из-под systemd"}' sudo tail -n 1 /var/lib/notes/notes.txt -
Оцени защиту и посмотри, каким пользователем идёт процесс.
tail -n 1оставляет итоговую строку,ps -o user,group,cmd -p <PID>печатает владельца, группу и команду процесса.systemd-analyze security notes --no-pager | tail -n 1 ps -o user,group,cmd -p "$(systemctl show notes -p MainPID --value)" -
Проверь границы песочницы отдельным экспериментом.
systemd-runзапускает одну команду как временный юнит с заданными свойствами-p:--quiet(без служебных сообщений),--wait(дождаться конца),--collect(убрать юнит после),--pipe(подключить вывод к твоему терминалу).sudo -u notes touch /var/lib/notes/x && echo "обычные права пускают"; sudo rm -f /var/lib/notes/x sudo systemd-run --quiet --wait --collect --pipe -p User=notes -p ProtectSystem=strict touch /var/lib/notes/x sudo systemd-run --quiet --wait --collect --pipe -p User=notes -p ProtectSystem=strict -p ReadWritePaths=/var/lib/notes touch /var/lib/notes/x sudo ls -l /var/lib/notes/x; sudo rm -f /var/lib/notes/x -
Ужесточи юнит: открой unit-файл в
~/notes/deploy/systemd/(vimилиnano), добавь в секцию[Service]две строкиProtectHome=trueиPrivateTmp=true(порядок любой, я ставлю послеNoNewPrivileges), затем переустанови и перезапусти. Так это выглядит из командной строки, если не хочется открывать редактор (sedдописывает строки после совпадения,aзначит append, урок 1.2):sed -i '/^NoNewPrivileges=true/a ProtectHome=true\nPrivateTmp=true' ~/notes/deploy/systemd/notes.service sudo install -m 644 ~/notes/deploy/systemd/notes.service /etc/systemd/system/notes.service sudo systemctl daemon-reload sudo systemctl restart notes sleep 1 systemd-analyze security notes --no-pager | tail -n 1 curl -s http://127.0.0.1:8080/healthzУбедись, что после правки
curlпо-прежнему отвечаетok, а оценка экспозиции стала меньше. -
Пока ничего не коммить. Unit-файл лежит в
~/notes/deploy/systemd/, но это обычная папка: git (система контроля версий, она запоминает историю изменений файлов) ты поставишь на «Заметки» в уроке 3.1, и файлdeploy/systemd/notes.serviceпопадёт в первый коммит вместе с остальным проектом. Сейчас достаточно, что он лежит в~/notesи установлен в/etc/systemd/system/. Файл/etc/notes/notes.envв git не попадёт и тогда: он живёт только на сервере (примерnotes.env.exampleпоявится позже, в уроках про конфигурацию).
Что должно получиться:
{"id": 1}
{"id": 1, "text": "Заметка из-под systemd", "created_at": "2026-09-30T11:54:31+00:00"}
→ Overall exposure level for notes.service: 8.7 EXPOSED :-(
notes notes /usr/bin/python3 /opt/notes/app.py
Шаг 3:
обычные права пускают
/usr/bin/touch: cannot touch '/var/lib/notes/x': Read-only file system
-rw-r--r-- 1 notes notes 0 Sep 30 12:00 /var/lib/notes/x
Шаг 4:
→ Overall exposure level for notes.service: 8.3 EXPOSED :-(
ok
Оценка (число и слово), время и id у тебя могут отличаться: важно, что после добавления ProtectHome и PrivateTmp число уменьшилось. Если у тебя в файле данных уже есть заметки, id будет больше.
Состояние проекта после урока: сервис notes управляется systemd, слушает 127.0.0.1:8080, данные в файле /var/lib/notes/notes.txt, приложение версии v2.2. Долги: нет HTTPS, нет прокси.
Как читать вывод:
{"id": 1}: ответ на POST, номер новой заметки.- Строка из
notes.txt: одна заметка это одна строка в формате JSON с полямиid,textиcreated_at(время создания в UTC). Overall exposure level ... 8.7 EXPOSED: итоговая оценка экспозиции, «чем меньше число, тем закрытее». После правки8.3.notes notes /usr/bin/python3 ...: процесс работает от пользователяnotesи группыnotes, не от root.Read-only file systemво втором эксперименте: песочница закрыла запись, хотя права Linux пускали (первая строка). Третья команда показывает, что исключениеReadWritePathsвозвращает запись.
Объясни себе: почему числовая оценка не должна доходить до 0? Что бы ты сделал, если бы сервису понадобилось писать логи в отдельный каталог? Чем два слоя (права Linux и песочница) отличаются?
Типичные ошибки:
Failed to restart notes.service: Unit notes.service has a bad unit file setting.: опечатка в ключе. Смотриjournalctl -xeu notesиsystemd-analyze verify /etc/systemd/system/notes.service.ProtectHome=trueне подействовал: забытdaemon-reload(вstatusпредупреждениеchanged on disk).
Сломай и почини
Скачай и запусти сценарий поломки. Файл break.sh не читай: цель в том, чтобы найти причину диагностикой. Скрипт создаёт файл-дополнение (drop-in, см. теорию) и правит настройку PORT в /etc/notes/notes.env, поэтому запускай его только на своём учебном сервере. Флаг -f у curl значит «при ошибке сервера не сохраняй страницу ошибки», -L разрешает переходить по перенаправлениям, -o задаёт имя файла:
curl -fsSL -o /tmp/break-1.8.sh https://raw.githubusercontent.com/distinguished-sre/learning/main/devops/project/notes/break/1.8/break.sh
sudo bash /tmp/break-1.8.sh 1
Сценарии: 1, 2 и 3. Проходи по одному: запусти, найди причину, почини сам или командой sudo bash /tmp/break-1.8.sh fix, потом бери следующий. В конце выполни fix и удали скрипт: rm /tmp/break-1.8.sh.
Симптом
- Сценарий 1. Через 10 секунд после запуска
systemctl is-active notesпишетactivating, аcurl -sS --max-time 3 http://127.0.0.1:8080/healthzзаканчиваетсяCouldn't connect to server. - Сценарий 2. Внешне то же самое:
activating,curlне отвечает. Причина другая. - Сценарий 3.
systemctl is-active notesпишетactive,curl /healthzотвечаетok, ноPOST /notesвозвращает{"error": "storage"}, а/readyzотвечает 503.
Сценарии 1 и 2 специально дают одинаковый симптом: отличить их можно только диагностикой.
Гипотезы
Прежде чем проверять, выпиши возможные причины. Для этих симптомов подходят:
- Неверный путь в
ExecStart(status203/EXEC). - Пользователь из
User=не существует (status217/USER). - Программа стартует и сама завершается с ошибкой (код выхода из её кода, например 2), а systemd перезапускает её по кругу.
- Правки в файле юнита есть, но
daemon-reloadне сделан: действует старая версия. - Приложение не может писать вне
ReadWritePaths: запись падает, а сервис при этом «жив». - Сервис упал слишком много раз за короткое время, и systemd сдался (
Start request repeated too quickly).
Проверки
systemctl status notes --no-pager # строка Process: и Active:, код в status=
journalctl -xeu notes --no-pager | tail -n 30
systemd-analyze verify /etc/systemd/system/notes.service
systemctl cat notes --no-pager # какие файлы реально применены, есть ли drop-in
systemctl show notes -p ReadWritePaths,ExecStart --no-pager
id notes # существует ли пользователь
sudo cat /etc/notes/notes.env # что программа получила из файла
ls -ld /var/lib/notes /opt/notes/app.py
Обрати внимание на строку Drop-In: в systemctl status и на последние блоки вывода systemctl cat: там появится файл break.conf, которого в твоём юните не было.
Разбор сценариев
Сценарий 1: status=203/EXEC. В systemctl status:
Drop-In: /etc/systemd/system/notes.service.d
└─break.conf
Active: activating (auto-restart) (Result: exit-code) since ...
Process: 924 ExecStart=/usr/local/bin/python3 /opt/notes/app.py (code=exited, status=203/EXEC)
systemctl cat notes в конце показывает файл break.conf с двумя строками ExecStart=: пустая сбрасывает прежнее значение, вторая задаёт неверный путь. systemd-analyze verify подтверждает: Command /usr/local/bin/python3 is not executable: No such file or directory. Проверка command -v python3 даёт /usr/bin/python3. Исправление: удалить дополнение (sudo rm /etc/systemd/system/notes.service.d/break.conf) или поправить путь в нём, затем обязательно sudo systemctl daemon-reload && sudo systemctl restart notes. Если ты исправил файл, но забыл daemon-reload, status напишет Warning: ... changed on disk, а сервис останется сломанным: это гипотеза 4 в действии.
Сценарий 2: программа завершается с кодом 2. В status: Process: ... (code=exited, status=2), а в журнале причина словами приложения:
python3[1085]: ошибка: PORT должен быть числом
systemd[1]: notes.service: Main process exited, code=exited, status=2/INVALIDARGUMENT
Раз ошибку выдала сама программа, смотри её настройки: sudo cat /etc/notes/notes.env показывает PORT=8080a. Исправление: sudo sed -i 's/^PORT=.*/PORT=8080/' /etc/notes/notes.env, затем sudo systemctl restart notes (daemon-reload не нужен: изменился файл настроек, а не юнит). Нюанс: при RestartSec=2 предохранитель Start request repeated too quickly не срабатывает, restart counter растёт (у нас дошёл до 10 за 30 секунд), а статус так и остаётся activating (auto-restart). Если бы юнит всё же дошёл до failed, после починки причины потребовался бы sudo systemctl reset-failed notes.
Сценарий 3: сервис жив, запись падает. systemctl show notes -p ReadWritePaths печатает пустое ReadWritePaths=, а systemctl cat notes показывает drop-in break.conf с пустой строкой ReadWritePaths=: пустое значение сбрасывает список исключений. Журнал говорит прямо:
python3[1174]: ошибка хранилища: [Errno 30] Read-only file system: '/var/lib/notes/notes.txt'
python3[1174]: 127.0.0.1 "POST /notes HTTP/1.1" 500 -
Обычные права при этом в порядке: sudo -u notes touch /var/lib/notes/x проходит. Значит, запись блокирует не Linux, а песочница systemd. Исправление: убрать дополнение, daemon-reload, restart. Проверь systemctl show notes -p ReadWritePaths (должно быть /var/lib/notes) и повтори POST.
Остальные гипотезы из списка проверь в своём воображении, они встречаются в реальных инцидентах:
217/USER: вstatusProcess: ... (code=exited, status=217/USER), в журналеFailed to determine user credentials: No such process. Причина: вUser=имя пользователя, которого нет. Исправление: создай пользователя (sudo useradd --system --no-create-home --shell /usr/sbin/nologin notes, как в уроке 1.3), проверь владельца/var/lib/notes, перезапусти.Start request repeated too quickly: это следствие, а не причина. Сначала найди реальную ошибку вjournalctl -u notes, почини, затемsudo systemctl reset-failed notes && sudo systemctl start notes.- Забыт
daemon-reload:systemctl statusпредупреждаетWarning: The unit file, source configuration file or drop-ins of notes.service changed on disk. Выполниsudo systemctl daemon-reload && sudo systemctl restart notes.
После починки проверь: systemctl is-active notes, curl -s http://127.0.0.1:8080/healthz и запись POST /notes.
ИИ в помощь
Нейросеть хорошо объясняет unit-файлы и строки журнала, но не видит твой сервер: что реально записано в юните и что лежит в журнале, она знает только из того, что ты покажешь. Общие правила работы с ней: ИИ-помощник.
Задача: разобрать unit-файл.
Вот unit-файл systemd: <вывод systemctl cat notes>. Объясни каждую строку. Что произойдёт при падении
процесса, при перезагрузке и при записи в /etc? Какие настройки безопасности стоят, а каких не хватает?
Предложи изменения, не применяя их.
Проверь ответ: прогони systemd-analyze verify и найди каждую настройку в man systemd.exec и man systemd.service. Типичная ошибка: нейросеть придумывает несуществующую настройку или советует Restart=always как лекарство от падений.
Задача: найти причину падения сервиса.
Сервис notes в статусе failed. systemctl status: <вывод>; journalctl -u notes -n 30: <вывод>.
Назови причину по строкам журнала, код выхода и что проверить дальше. Сначала только проверки,
ничего не меняющие.
Проверь ответ: убедись, что названные строки есть в твоём выводе, и проверь путь, права и пользователя из ExecStart. Типичная ошибка: советует перезапускать сервис вместо поиска причины.
Задача: выбрать команды vim для правки.
Мне нужно в vim: найти строку Restart=, заменить значение, сохранить и выйти, а при ошибке выйти
без сохранения. Напиши последовательность клавиш по шагам и скажи, в каком режиме я нахожусь.
Проверь ответ: проверь последовательность на копии файла. Типичная ошибка: пропущен Esc перед :wq.
Словарик урока
| Термин | Простыми словами |
|---|---|
| systemd | первая программа системы (PID 1): запускает сервисы, следит за ними, собирает логи |
| сервис (service), демон | программа, которая должна работать постоянно без участия человека |
| юнит (unit) | описание того, чем управляет systemd; для сервиса это файл *.service |
| секция, ключ | [Service] - раздел файла юнита; Restart=on-failure - настройка в разделе |
| цель (target) | точка на пути загрузки; multi-user.target это обычное рабочее состояние сервера |
daemon-reload |
«systemd, перечитай файлы юнитов»: без него правки на диске не действуют |
| drop-in | каталог имя.service.d/ с файлами *.conf, которые дополняют или заменяют строки юнита |
enable и start |
start запускает сейчас, enable включает автозапуск при загрузке |
| символическая ссылка (symlink) | файл-указатель на другой файл; так enable подключает юнит к цели |
| переменная окружения | пара «имя = значение», которую процесс получает при запуске (PORT=8080) |
EnvironmentFile |
файл, из которого systemd берёт переменные окружения для сервиса |
код 203/EXEC, 217/USER |
ошибки запуска: не удалось выполнить команду; не удалось переключиться на пользователя |
Restart=on-failure |
поднимать сервис снова, если он завершился с ошибкой или был убит |
| предохранитель (start limit) | защита: больше 5 запусков за 10 секунд, и systemd перестаёт запускать |
reset-failed |
сбросить отметку «сломан» и счётчик перезапусков |
| journal, journald | единое хранилище логов всех сервисов и служба, которая его ведёт |
journalctl |
команда для чтения журнала, с фильтрами по юниту, времени, важности |
| pager | просмотрщик длинного вывода (less): листается стрелками, выход q |
| приоритет (priority) | важность записи от 0 (emerg) до 7 (debug); -p err берёт 0-3 |
| hardening | ограничение прав сервиса: только чтение файловой системы, нет прав повышаться и так далее |
ProtectSystem=strict |
вся файловая система для сервиса только для чтения |
ReadWritePaths |
исключение: каталоги, куда писать всё-таки можно |
| cgroup | механизм ядра: объединяет процессы в группу и считает или ограничивает их ресурсы |
systemd-analyze security |
оценка «открытости» юнита от 0 до 10 |
| vim | консольный редактор с режимами (обычный и вставки) |
| nano | простой консольный редактор с подсказками внизу |
| режим (mode) | состояние vim: в обычном клавиши это команды, в режиме вставки это текст |
| регистр vim | временное хранилище: dd кладёт туда строку, p вставляет |
sudoedit |
безопасный способ править системный файл: редактор работает от твоего пользователя над копией |
| swap-файл | скрытый файл .имя.swp с копией несохранённых правок vim |
Вопросы с собеседований
Раздел для повторения: ответь вслух, потом открой ответ. Короткие вопросы с пометкой [на скорость] тренируй на время: ответ за 30 секунд.
1. [junior] [часто] Ты поправил unit-файл, systemctl restart не изменил поведение. Почему?
Ответ
Скорее всего, я не сделал systemctl daemon-reload, и systemd держит старую версию юнита в памяти. restart перезапускает процесс, но не перечитывает файл. Проверяю systemctl status: там будет предупреждение changed on disk, и systemctl cat, чтобы увидеть, какие файлы применены (в том числе drop-in). Затем daemon-reload и restart.
Что хотят услышать: daemon-reload, systemctl cat, отличие drop-in от основного файла, systemctl edit.
Красный флаг: «перезагружу сервер» или не знает, что systemd держит юниты в памяти.
2. [junior] [часто] Чем enable отличается от start?
Ответ
start запускает сервис сейчас, enable включает автозапуск при загрузке: создаёт символическую ссылку в каталоге цели (multi-user.target.wants/). enable --now делает обе вещи. Обратное: disable и stop. Состояние проверяют через is-enabled и is-active.
Что хотят услышать: независимость двух действий, is-enabled и is-active, ссылка в .wants.
Красный флаг: считает, что enable запускает сервис.
3. [middle] [часто] Нужно найти, что произошло с сервисом вчера в 03:00. Как читаешь логи?
Ответ
journalctl -u <сервис> --since "yesterday 02:50" --until "yesterday 03:10" --no-pager. Если сервер перезагружался, добавляю -b -1 (предыдущая загрузка). Для аварийного завершения смотрю ещё journalctl -k (сообщения ядра) и systemctl status. Всё это работает, только если журнал хранится на диске (/var/log/journal), иначе после перезагрузки он пуст.
Что хотят услышать: --since и --until, -b -1, постоянный журнал (/var/log/journal), -p для фильтра.
Красный флаг: говорит, что журнал «пропал после перезагрузки» и не знает про Storage=persistent.
4. [junior] Сервис в статусе failed, status=203/EXEC. Что делаешь?
Ответ
Смотрю journalctl -xeu <сервис> и systemctl cat. 203/EXEC значит, что systemd не смог выполнить команду из ExecStart: неверный путь, нет права на исполнение или нет интерпретатора, указанного в первой строке скрипта (shebang, строка #!/...). Проверяю путь через ls -l и command -v, исправляю, делаю daemon-reload и restart.
Что хотят услышать: коды 203 и 217 (ошибки до запуска программы), абсолютный путь, право x, чтение журнала.
Красный флаг: запускает ExecStart руками под root и делает вывод «у меня работает» (у сервиса другой пользователь и песочница).
5. [junior] [на скорость] Как выйти из vim без сохранения? Ты случайно набрал текст в обычном режиме.
Ответ
Esc (чтобы точно быть в обычном режиме), затем :q! и Enter. Если хочу отменить только последние действия, нажимаю u. Если файл только для чтения и я уже успел его изменить, сохраняю через :w !sudo tee % или, лучше, открываю такие файлы сразу через sudoedit.
Что хотят услышать: режимы vim, :q!, :wq, sudoedit.
Красный флаг: «закрою терминал» или паника без вариантов.
6. [middle] Сервис падает и перезапускается каждые две секунды. Что у systemd пишет Start request repeated too quickly и как действовать?
Ответ
Это предохранитель: если за StartLimitIntervalSec (10 секунд по умолчанию) было больше StartLimitBurst (5) запусков, systemd перестаёт пытаться и переводит юнит в failed. Это следствие, а не причина. Сначала не «лечу» лимит, а ищу причину падения: journalctl -u <сервис>, код выхода, при необходимости coredump (файл с содержимым памяти упавшей программы). Починив причину, делаю systemctl reset-failed. Временно ослаблять лимит можно, но постоянно это маскирует проблему. Нюанс: с RestartSec=2 предохранитель обычно не срабатывает вообще, а сервис вечно висит в activating (auto-restart).
Что хотят услышать: предохранитель как следствие, reset-failed, RestartSec, поиск причины в журнале.
Красный флаг: отключает лимит или ставит Restart=always с RestartSec=0 «чтобы работало».
7. [middle] Приложение живо, systemctl status зелёный, но данные не пишутся. Где искать?
Ответ
Проверяю, что реально применил systemd: systemctl cat, systemctl show -p ReadWritePaths,ProtectSystem. При ProtectSystem=strict запись вне ReadWritePaths падает с Read-only file system, хотя права Unix в порядке. Ещё проверяю владельца каталога и права пользователя из User=, затем читаю журнал.
Что хотят услышать: песочница systemd поверх прав Unix, ReadWritePaths, ProtectHome, PrivateTmp, журнал.
Красный флаг: делает chmod 777 на каталог данных.
8. [middle] Как передать сервису пароль от БД? Что нельзя делать?
Ответ
Положить его в EnvironmentFile с правами 640 root:<группа сервиса>, а не в юнит-файл: Environment= виден всем через systemctl show. Лучше LoadCredential= (systemd передаёт сервису секрет как файл в закрытом каталоге) или внешний менеджер секретов. В git секрет не попадает.
Что хотят услышать: EnvironmentFile, права 640, LoadCredential, секреты не в юните и не в git.
Красный флаг: пароль в ExecStart командной строкой (виден всем в ps).
9. [middle] Сервис жив, но за ночь съел всю память и его убил OOM-killer. Как сделать, чтобы это не тронуло остальной сервер?
Ответ
Задаю лимиты cgroup в юните: MemoryMax= (жёсткий потолок), MemoryHigh= (мягкий, после него ядро начинает придерживать сервис), при необходимости TasksMax=. При превышении страдает только этот сервис, а Restart=on-failure его поднимет. Лимит только сдерживает ущерб, причину утечки ищу отдельно (метрики памяти, профилирование).
Что хотят услышать: cgroups через systemd, MemoryMax, journalctl -k | grep -i oom (сообщения ядра), systemd-cgtop, различие лимита и лечения.
Красный флаг: «добавлю RAM» или «поставлю cron, который перезапускает раз в сутки» как единственное решение.
10. [junior] [на скорость] Чем сервис под systemd лучше запуска через nohup или tmux?
Ответ
nohup и tmux переживают закрытие терминала, но не перезагрузку и не падение. systemd поднимает сервис при загрузке, перезапускает по политике, собирает логи в журнал, ограничивает ресурсы и права, показывает состояние одной командой.
Что хотят услышать: автозапуск, Restart, журнал, изоляция, единый интерфейс управления.
Красный флаг: «и так работает, зачем усложнять».
11. [middle] Зачем sudoedit, если есть sudo vim?
Ответ
sudo vim запускает весь редактор с правами root: из него можно выполнить любую команду (:!bash), а плагины и настройки vim тоже работают от root. sudoedit открывает временную копию файла редактором от обычного пользователя, а sudo записывает её на место оригинала только при сохранении. Права и владелец оригинала сохраняются.
Что хотят услышать: минимальные привилегии, временная копия, EDITOR.
Красный флаг: «мне удобнее сразу под root, что такого».
12. [junior] Чем systemctl restart отличается от reload?
Ответ
restart останавливает сервис и запускает заново: процесс новый, соединения рвутся. reload просит сервис перечитать конфиг без перезапуска процесса (обычно через сигнал вроде SIGHUP), и работает только у сервиса, который умеет перечитывать конфиг: обычно в unit описан ExecReload, бывает ещё Type=notify-reload. Например, nginx после правки конфига безопаснее перечитать, проверив nginx -t. Если отдельного reload у сервиса нет, есть reload-or-restart. Но не всякую настройку можно применить на лету, тогда нужен restart.
Что хотят услышать: restart останавливает процесс, reload перечитывает конфиг, ExecReload, reload-or-restart, проверка конфига.
Красный флаг: «reload - это то же самое, только быстрее».
13. [middle] Что значат After=, Wants= и Requires= в unit-файле?
Ответ
After= и Before= задают только порядок запуска и ничего сами не запускают. Wants= - слабая зависимость: указанный unit будет запущен вместе с нашим, но если он не стартует, наш всё равно запустится. Requires= - строгая: если зависимость не запустилась, наш unit тоже не стартует, но только когда есть и After=. Поэтому обычно пишут пару, например Requires=postgresql.service и After=postgresql.service: сначала зависимость, потом сервис. Для сети часто ставят After=network-online.target и Wants=network-online.target. Граф зависимостей смотрю командой systemctl list-dependencies имя.
Что хотят услышать: порядок отдельно от зависимости, Wants слабее Requires, пара Requires+After, network-online.target, list-dependencies.
Красный флаг: «After= запустит нужный сервис».
14. [middle] Чем Type=simple, forking и oneshot отличаются в systemd?
Ответ
simple - по умолчанию: процесс из ExecStart и есть сервис, он остаётся на переднем плане. forking нужен старым демонам, которые сами уходят в фон: systemd ждёт завершения родителя и следит за дочерним, часто по PID-файлу (PIDFile=). oneshot - для разовой задачи: systemd ждёт завершения команды, и unit считается выполненным (часто с RemainAfterExit=yes). Современные приложения лучше запускать на переднем плане как simple. Неверный тип даёт симптомы вроде «сервис запущен, но systemd считает его упавшим».
Что хотят услышать: simple - процесс на переднем плане, forking и PID-файл, oneshot для разовых задач, RemainAfterExit, симптомы неверного типа.
Красный флаг: «Тип не важен, всегда ставлю forking».
Проверено на версиях
- Ubuntu 24.04.5 LTS (контейнер Docker с systemd, ядро 6.12): вся практика заданий 1-5, сценарии 1-3 и
fixиз «Сломай и почини» прогнаны целиком. systemd 255 (255.4-1ubuntu8.17), Python 3.12.3, vim 9.1 (2024 Jan 02), nano 7.2, git 2.43.0, приложение «Заметки» v2.2 (эталонproject/notes/versions/v2.2.py). - Vim прогонялся в настоящем терминальном сеансе (tmux), сообщения
E37,E45,E325,E486,W10,W12скопированы с экрана. - Перезагрузка проверена перезапуском контейнера (
docker restart): после неё сервисactive,enabled,curl /healthzвернулok. Командаjournalctl -b -1(предыдущая загрузка) не проверялась: в контейнере журнал не разделяет загрузки. - Ubuntu 26.04.1 LTS: полный прогон не выполнялся, так как обычный образ без systemd. Проверено только:
systemd-analyze verify(systemd 259.5-0ubuntu3.4) и его сообщение об опечатке в ключе (Unknown key 'ExecStar' in section [Service], ignoring., отличается от 24.04), запускapp.pyv2.2 на Python 3.14.4 и вид access-лога, версии vim 9.1.2141 и nano 8.7.1, curl 8.18 (Could not connect to server). Не прогонялось:systemctl status,journalctl,systemd-analyze securityна 26.04. - Не проверялось: лимиты
MemoryMax(эксперимент на стенде не дал предсказуемого результата, в уроке описана только идея),LoadCredential,Storage=persistent, работа под WSL2. - Оценка
systemd-analyze security(8.7 и 8.3) снята на стенде в контейнере, на обычной ВМ может отличаться.
Итог урока: ты умеешь
- умею написать unit-файл с
User,EnvironmentFile,Restart=on-failureи установить его в/etc/systemd/system/ - умею выполнить
daemon-reload,enable --nowи проверить состояниеstatusиis-active - умею убить сервис и показать, что systemd его поднял, а после
stopне поднял - умею читать журнал:
journalctl -u,-f,--since,-p,-b - умею по коду
203/EXEC,217/USERиStart request repeated too quicklyнайти причину - умею ограничить сервис:
ProtectSystem=strict,ReadWritePaths,NoNewPrivileges, проверитьsystemd-analyze security - умею править файлы в vim и nano, выходить с сохранением и без, использовать
sudoedit
Дальше: Тема 2: Сеть, HTTP, DNS, nginx и TLS
Проверь себя
Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.
Тест работает с включённым JavaScript.