Learning Platform
Глоссарий Troubleshooting
Урок 21.04 · 22 мин
Средний
CapstonesystemdTimerService unitDeploy

Deploy через systemd: service + timer

cleanup.sh готов и протестирован. Теперь — деплой на server. Цель: скрипт запускается автоматически каждую ночь, логи попадают в journald, при failure systemd может ретраить или alertить. Modern Linux way — через systemd service + timer, а не cron.

В этом уроке: создание service unit (что запускать), timer unit (когда запускать), активация через systemctl, verification через journalctl. После этого Junior умеет деплоить любой scheduled job на production server.


Почему systemd, а не cron

systemd timer vs cron
cronLegacy, ubiquitous. Простой синтаксис. Минимально features. Логи в stdout/stderr -> /var/mail или редирект в файл
вытесняется
systemd timerModern (systemd). Structured journald logs, status через systemctl, Persistent для catch-up, RandomizedDelaySec, resource limits, dependencies
*/5 * * * *cron syntax — minute hour day month weekday. Краткий, но cryptic
OnCalendar=dailysystemd syntax — calendar specifiers (daily, hourly, *-*-* HH:MM:SS). Читаемее, гибче

Преимущества systemd timer:

  1. journalctl logs — structured, queryable: journalctl -u airflow-log-cleanup. cron логи в /var/log/syslog или mail — поиск пытка.
  2. systemctl status — last run, exit code, next run, время выполнения.
  3. Persistent=true — если timer должен был сработать во время downtime, после boot запускается catch-up. cron такого не умеет.
  4. RandomizedDelaySec — distribute load (если 100 серверов запускают одно и то же — distribute).
  5. Resource limits: MemoryMax, CPUQuota, IOWeight.
  6. Dependencies: Wants=, After=, Requires= — запустить только после network.target.
  7. Security: User=, Group=, PrivateTmp=true, ProtectSystem=.

cron остаётся для legacy и совсем простого. Modern systems — systemd.

Kubernetes CronJob — тот же паттерн, но в кластере

Service unit: что запускать

/etc/systemd/system/airflow-log-cleanup.service:

[Unit]
Description=Airflow log cleanup and archival
Documentation=https://github.com/acme-corp/ops-tools/blob/main/airflow-log-cleanup/README.md
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=airflow
Group=airflow

# Path to script and arguments
ExecStart=/opt/airflow-ops/bin/cleanup.sh \
    --log-dir /var/log/airflow \
    --retention-days 7 \
    --s3-bucket acme-airflow-archive

# Environment
Environment="SLACK_WEBHOOK=https://hooks.slack.com/services/T00/B00/XXX"
Environment="AWS_DEFAULT_REGION=eu-central-1"

# Logging — pipe stderr/stdout to journald
StandardOutput=journal
StandardError=journal
SyslogIdentifier=airflow-log-cleanup

# Resource limits (защита от runaway)
MemoryMax=512M
CPUQuota=50%

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/log/airflow /var/run /tmp
PrivateTmp=true
ProtectHome=true

# Не рестартить — это oneshot
Restart=no

[Install]
WantedBy=multi-user.target

Разбор сектций

[Unit]:

  • Description= — человеко-читаемое имя, видно в systemctl list-units.
  • Documentation= — ссылка на docs/README. Хорошая практика для onboarding.
  • After=network-online.target — запускать после того, как сеть готова (нужно для S3 upload и Slack).
  • Wants=network-online.target — soft dependency, не требует но желает.

[Service]:

  • Type=oneshot — выполнить один раз, exit. (Альтернативы: simple — long-running daemon, forking — daemonized process, notify — отправляет systemd notification).
  • User=airflow Group=airflow — запускать под этим user, не root. Принцип least privilege.
  • ExecStart= — путь к скрипту + аргументы. Многострочный через \. Полный путь обязателен, systemd не использует PATH.
  • Environment= — env vars. Не для секретов (видны в systemctl show). Для секретов — EnvironmentFile=/etc/airflow-ops/secrets.env с chmod 600.
  • StandardOutput=journal StandardError=journal — pipe stdout/stderr в journald. По умолчанию для service это default, но явное лучше.
  • SyslogIdentifier= — тег в journal. journalctl -t airflow-log-cleanup.
  • MemoryMax=512M CPUQuota=50% — cgroup-based limits. Защита от memory leak.
  • NoNewPrivileges=true — нельзя setuid/setcap escalate.
  • ProtectSystem=strict — read-only /, /usr, /etc, /boot.
  • ReadWritePaths= — exceptions от ProtectSystem.
  • PrivateTmp=true — отдельный /tmp namespace, не виден другим процессам.
  • ProtectHome=true — /home, /root недоступны.
  • Restart=no — для oneshot не рестартим. (Для long-running daemon — Restart=on-failure).

[Install]:

  • WantedBy=multi-user.target — куда символлинковать при systemctl enable. multi-user.target — стандарт для server processes.
Service unit anatomy
[Unit]Metadata + dependencies. Description, Documentation, After (порядок старта), Wants/Requires (soft/hard deps)
[Service]Что и как запускать. Type, User, ExecStart, Environment, security, resource limits
[Install]WantedBy — куда подключать при enable. multi-user.target для normal servers

Timer unit: когда запускать

/etc/systemd/system/airflow-log-cleanup.timer:

[Unit]
Description=Daily Airflow log cleanup timer
Documentation=https://github.com/acme-corp/ops-tools

[Timer]
# Daily at 03:00
OnCalendar=*-*-* 03:00:00

# Add up to 10 minutes random delay (для distribute load если много серверов)
RandomizedDelaySec=10m

# If missed (server was off) — run on next boot
Persistent=true

# Связанный service unit (по умолчанию = .timer name с .service суффиксом)
Unit=airflow-log-cleanup.service

[Install]
WantedBy=timers.target

OnCalendar syntax

*-*-* 03:00:00       # daily at 03:00
Mon..Fri 09:00:00    # weekdays at 09:00
*-*-01 04:00:00      # 1st of each month at 04:00
hourly               # каждый час (alias for *-*-* *:00:00)
daily                # каждый день в 00:00 (alias for *-*-* 00:00:00)
weekly               # понедельник в 00:00
monthly              # 1-е число месяца в 00:00
*:0/5                # каждые 5 минут

Проверка калькулятора:

$ systemd-analyze calendar 'Mon..Fri 09:00:00'
Original form: Mon..Fri 09:00:00
Normalized form: Mon..Fri *-*-* 09:00:00
    Next elapse: Mon 2026-05-18 09:00:00 UTC
       In UTC: Mon 2026-05-18 09:00:00 UTC
       From now: 4 days left

Полезно при сложных expressions для verification.

Persistent=true

Без Persistent=true: timer триггерится только в указанное время. Если server down 03:00 — миссия missed, до next 03:00.

С Persistent=true: systemd запоминает last run в /var/lib/systemd/timers/. После boot — проверка, что времени с last run прошло > interval — catch-up запуск.

Это критично для batch jobs, которые должны запуститься каждый day, даже если server reboot.

RandomizedDelaySec

Если у тебя 100 серверов с одним и тем же timer — все 100 запустятся одновременно. Может задавить downstream (например, S3 API rate limit, network bandwidth, Slack webhook).

RandomizedDelaySec=10m — каждый server добавляет random offset до 10 минут. Распределение в окне 03:00-03:10. Defensive design.


Deploy: установка и активация

# 1. Положить скрипт:
sudo install -o root -g root -m 0755 cleanup.sh /opt/airflow-ops/bin/cleanup.sh

# 2. Положить service unit:
sudo install -o root -g root -m 0644 airflow-log-cleanup.service /etc/systemd/system/

# 3. Положить timer unit:
sudo install -o root -g root -m 0644 airflow-log-cleanup.timer /etc/systemd/system/

# 4. Reload systemd, чтобы он увидел новые units:
sudo systemctl daemon-reload

# 5. Enable timer (создаёт symlink, runs on boot):
sudo systemctl enable airflow-log-cleanup.timer

# 6. Start timer (immediate start без перезагрузки):
sudo systemctl start airflow-log-cleanup.timer

Или одной командой enable --now:

sudo systemctl enable --now airflow-log-cleanup.timer

Verify deployment

# Статус timer:
$ systemctl status airflow-log-cleanup.timer
 airflow-log-cleanup.timer - Daily Airflow log cleanup timer
     Loaded: loaded (/etc/systemd/system/airflow-log-cleanup.timer; enabled)
     Active: active (waiting) since Mon 2026-05-13 16:42:01 UTC
    Trigger: Tue 2026-05-14 03:04:23 UTC; 10h left
   Triggers: airflow-log-cleanup.service

# Список всех timers (видим next trigger):
$ systemctl list-timers --all
NEXT                        LEFT          LAST    PASSED  UNIT
Tue 2026-05-14 03:04:23 UTC 10h left      n/a     n/a     airflow-log-cleanup.timer
Tue 2026-05-14 04:00:00 UTC 11h left      ...     ...     logrotate.timer

# Manual trigger для теста (запустить service сразу, не ждать timer):
$ sudo systemctl start airflow-log-cleanup.service

# Посмотреть logs:
$ journalctl -u airflow-log-cleanup -n 50 --no-pager
May 13 16:42:01 prod-airflow systemd[1]: Starting Airflow log cleanup and archival...
May 13 16:42:01 prod-airflow cleanup.sh[12345]: 2026-05-13T16:42:01+00:00 [INFO] cleanup.sh: Starting cleanup.sh v1.0.0
May 13 16:42:02 prod-airflow cleanup.sh[12345]: 2026-05-13T16:42:02+00:00 [INFO] cleanup.sh: Found 245 candidate files
May 13 16:43:30 prod-airflow cleanup.sh[12345]: 2026-05-13T16:43:30+00:00 [INFO] cleanup.sh: Compressed 245 files, freed 12345678 bytes
May 13 16:43:35 prod-airflow systemd[1]: airflow-log-cleanup.service: Deactivated successfully.

# Tail logs в реальном времени:
$ journalctl -u airflow-log-cleanup -f
Deploy и verify workflow
installinstall -o root -g root -m 0755 для скрипта (executable), 0644 для unit-файлов. Не chmod 777 — security
daemon-reloadsystemd сам не сканирует /etc/systemd/system при изменениях, нужно явно перечитать
enable --now--now запускает + enables (symlinks для autostart). Эквивалент enable + start
systemctl statusТекущий статус, last run, next trigger
list-timersВсе timers в системе, отсортированы по next run
journalctl -uPersistent logs, queryable, structured. journalctl -u name -f для tail

Real-world ops commands

# Disable + stop:
sudo systemctl disable --now airflow-log-cleanup.timer

# Force-run сейчас (вне расписания):
sudo systemctl start airflow-log-cleanup.service

# Перезагрузить unit после edit:
sudo systemctl daemon-reload
sudo systemctl restart airflow-log-cleanup.timer

# Список всех timers:
systemctl list-timers --all

# Logs за последние 24 часа:
journalctl -u airflow-log-cleanup --since '24 hours ago'

# Logs только errors:
journalctl -u airflow-log-cleanup -p err

# Logs за конкретный run (между двумя timestamps):
journalctl -u airflow-log-cleanup --since '2026-05-13 03:00' --until '2026-05-13 03:30'

# JSON output (для CI/мониторинга):
journalctl -u airflow-log-cleanup --output=json --since '24 hours ago' | jq

# Disk usage journald:
journalctl --disk-usage
# Cleanup старых logs:
sudo journalctl --vacuum-time=30d

Hardening и production patterns

EnvironmentFile для секретов

Не клади secrets в Environment= (видны в systemctl show). Используй EnvironmentFile:

# В service unit:
EnvironmentFile=/etc/airflow-ops/secrets.env
# /etc/airflow-ops/secrets.env (chmod 600, owned by service user):
SLACK_WEBHOOK=https://hooks.slack.com/services/T00/B00/XXX
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
sudo chmod 600 /etc/airflow-ops/secrets.env
sudo chown airflow:airflow /etc/airflow-ops/secrets.env

Только service user (airflow) может прочитать. Environment= evident в systemctl show, EnvironmentFile= — нет (только путь).

OnFailure для alerting

[Unit]
OnFailure=airflow-log-cleanup-failed.service
# /etc/systemd/system/airflow-log-cleanup-failed.service:
[Unit]
Description=Alert on cleanup failure
[Service]
Type=oneshot
ExecStart=/usr/bin/curl -X POST -d '{"text":"FAIL"}' $SLACK_URL

Если cleanup.service exit != 0 — systemd запускает failed.service. Дополнительный уровень alerting помимо встроенного slack_notify в самом скрипте.

journald disk limits

# /etc/systemd/journald.conf
[Journal]
SystemMaxUse=2G        # journald использует max 2GB
SystemMaxFileSize=200M # каждый файл лога max 200MB
MaxRetentionSec=30d    # хранить 30 дней

Без этих лимитов journald может съесть весь диск — что не лучше overflowing /var/log/airflow.


Testing на dev перед production

Перед enable --now на production:

  1. shellcheck cleanup.sh — clean.
  2. bats test_cleanup.bats — all pass.
  3. systemd-analyze verify airflow-log-cleanup.service — unit file валиден.
  4. Dry-run service:
sudo systemctl start airflow-log-cleanup.service
journalctl -u airflow-log-cleanup -n 50
# Видим что job отработал correctly
  1. Timer dry-run (manual trigger):
sudo systemctl start airflow-log-cleanup.timer
systemctl list-timers
  1. Production install — только после всех проверок.

  • Урок 03 (implementation) — где cleanup.sh.
  • LAB-04 — практика. README.md проводит через deploy step-by-step.
  • Модуль 14 (systemd) — основы systemd.
  • Модуль 15 (cron) — сравнение с cron.

Попробуй сам

  1. Скопируй service и timer unit-файлы из этого урока. Адаптируй пути под свою dev-машину.

  2. Установи в /etc/systemd/system/, сделай daemon-reload.

  3. Validate units:

systemd-analyze verify /etc/systemd/system/airflow-log-cleanup.service
systemd-analyze verify /etc/systemd/system/airflow-log-cleanup.timer
  1. Enable + start:
sudo systemctl enable --now airflow-log-cleanup.timer
  1. Verify status:
systemctl status airflow-log-cleanup.timer
systemctl list-timers
  1. Force-run сейчас для теста:
sudo systemctl start airflow-log-cleanup.service
journalctl -u airflow-log-cleanup -n 50
  1. Edit timer (поменяй на каждые 5 минут для теста: OnCalendar=*:0/5), reload, watch:
sudo systemctl edit airflow-log-cleanup.timer
# (или прямо в файле + daemon-reload)
journalctl -u airflow-log-cleanup -f
  1. Cleanup (disable, удалить):
sudo systemctl disable --now airflow-log-cleanup.timer
sudo rm /etc/systemd/system/airflow-log-cleanup.{service,timer}
sudo systemctl daemon-reload

Проверка знанийKnowledge check
После deploy капстоуна на production server, manager спрашивает: 'А что если server reboot в 02:50, и timer должен был сработать в 03:00 пока сервер был down?' Объясни, как Persistent=true решает эту проблему и в чём принципиальное отличие от cron в этом сценарии.
ОтветAnswer
**Без Persistent=true**: timer триггерится только когда наступает указанное время И systemd активен. Если сервер был выключен в 03:00 — миссия missed, до следующего 03:00 (через 24 часа). Нет catch-up. **С Persistent=true**: systemd запоминает last successful run timer в /var/lib/systemd/timers/airflow-log-cleanup.timer. После boot systemd проверяет: если timestamp last run < (current time - interval), значит triggered missed — запускает service сразу (catch-up). Practical: server boot в 04:30 — systemd проверяет, last run был вчера в 03:00, ожидаемый next был 03:00 сегодня, прошло 25.5 часов > interval. Запускает service в 04:30. **Отличие от cron**: cron НЕ имеет встроенного catch-up. anacron — отдельная программа specifically для periodic jobs с catch-up, но это отдельная install + другой crontab формат. По умолчанию cron jobs во время downtime теряются навсегда. systemd timer объединяет periodic timing + catch-up в одном механизме. **Когда это критично**: nightly ETL, backups, log rotation, certificate renewal — задачи, которые ДОЛЖНЫ выполниться каждый день. Без Persistent=true их можно потерять незаметно (миссия пропущена, никто не заметил, через неделю обнаружили что disk полный). Это одна из главных причин миграции с cron на systemd timers. Bonus features systemd: RandomizedDelaySec (distribute load если 100 серверов), structured journald logs (journalctl -u name -f), systemctl status (immediate visibility), OnFailure handler (отдельная service для alerting). Production-grade scheduling.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Что делает `Type=oneshot` в systemd service unit?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4