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:
- journalctl logs — structured, queryable:
journalctl -u airflow-log-cleanup. cron логи в /var/log/syslog или mail — поиск пытка. - systemctl status — last run, exit code, next run, время выполнения.
- Persistent=true — если timer должен был сработать во время downtime, после boot запускается catch-up. cron такого не умеет.
- RandomizedDelaySec — distribute load (если 100 серверов запускают одно и то же — distribute).
- Resource limits:
MemoryMax,CPUQuota,IOWeight. - Dependencies:
Wants=,After=,Requires=— запустить только после network.target. - 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=airflowGroup=airflow— запускать под этим user, не root. Принцип least privilege.ExecStart=— путь к скрипту + аргументы. Многострочный через\. Полный путь обязателен, systemd не использует PATH.Environment=— env vars. Не для секретов (видны вsystemctl show). Для секретов —EnvironmentFile=/etc/airflow-ops/secrets.envс chmod 600.StandardOutput=journalStandardError=journal— pipe stdout/stderr в journald. По умолчанию для service это default, но явное лучше.SyslogIdentifier=— тег в journal.journalctl -t airflow-log-cleanup.MemoryMax=512MCPUQuota=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.
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
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:
- shellcheck cleanup.sh — clean.
- bats test_cleanup.bats — all pass.
- systemd-analyze verify airflow-log-cleanup.service — unit file валиден.
- Dry-run service:
sudo systemctl start airflow-log-cleanup.service
journalctl -u airflow-log-cleanup -n 50
# Видим что job отработал correctly
- Timer dry-run (manual trigger):
sudo systemctl start airflow-log-cleanup.timer
systemctl list-timers
- Production install — только после всех проверок.
Cross-links
- Урок 03 (implementation) — где cleanup.sh.
- LAB-04 — практика. README.md проводит через deploy step-by-step.
- Модуль 14 (systemd) — основы systemd.
- Модуль 15 (cron) — сравнение с cron.
Попробуй сам
-
Скопируй service и timer unit-файлы из этого урока. Адаптируй пути под свою dev-машину.
-
Установи в
/etc/systemd/system/, сделайdaemon-reload. -
Validate units:
systemd-analyze verify /etc/systemd/system/airflow-log-cleanup.service
systemd-analyze verify /etc/systemd/system/airflow-log-cleanup.timer
- Enable + start:
sudo systemctl enable --now airflow-log-cleanup.timer
- Verify status:
systemctl status airflow-log-cleanup.timer
systemctl list-timers
- Force-run сейчас для теста:
sudo systemctl start airflow-log-cleanup.service
journalctl -u airflow-log-cleanup -n 50
- Edit timer (поменяй на каждые 5 минут для теста:
OnCalendar=*:0/5), reload, watch:
sudo systemctl edit airflow-log-cleanup.timer
# (или прямо в файле + daemon-reload)
journalctl -u airflow-log-cleanup -f
- Cleanup (disable, удалить):
sudo systemctl disable --now airflow-log-cleanup.timer
sudo rm /etc/systemd/system/airflow-log-cleanup.{service,timer}
sudo systemctl daemon-reload