Writing a systemd Unit File Precisely
한국어 원문으로 표시합니다.
한 줄 요약
유닛 파일에서 가장 중요한 한 줄은 Type= 이다. systemd 가 "이 서비스가 언제 시작 완료됐는가" 를 판단하는 기준이기 때문이다.
왜 이게 필요했나
systemctl start myapp 이 30초 동안 멈춰 있다가 실패한다. 그런데 프로세스는 잘 떠 있다. 이런 상황의 원인은 대개 Type 이다.
어떻게 동작하나
유닛 파일 위치와 우선순위
| 경로 | 우선순위 | 용도 |
|---|---|---|
/etc/systemd/system/ |
높음 | 관리자 커스텀 유닛(오버라이드) |
/run/systemd/system/ |
중간 | 런타임 생성 |
/usr/lib/systemd/system/ |
낮음 | 패키지 설치 기본 유닛 |
패키지가 제공하는 유닛을 직접 고치지 마라. 업데이트에서 덮여 사라진다. systemctl edit <유닛> 으로 /etc/systemd/system/<유닛>.d/override.conf 를 만드는 것이 정석이다.
세 섹션
[Unit]
Description=My Web Application
Documentation=https://wiki.internal/myapp
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service
ConditionPathExists=/etc/myapp/config.yaml
[Service]
Type=notify
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=-/etc/myapp/env
ExecStartPre=/opt/myapp/bin/check-config --validate
ExecStart=/opt/myapp/bin/server --config /etc/myapp/config.yaml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5
TimeoutStartSec=30
StandardOutput=journal
SyslogIdentifier=myapp
[Install]
WantedBy=multi-user.target
After 와 Requires 는 다르다. After 는 순서만 정하고, Requires 는 의존을 만든다. 순서 없이 의존만 걸면 두 서비스가 동시에 시작될 수 있다. 보통 둘을 함께 쓴다.
EnvironmentFile=- 의 하이픈은 "파일이 없어도 실패하지 않는다" 는 뜻이다.
Type 비교
| Type | 시작 완료 판단 | 적합한 서비스 |
|---|---|---|
simple(기본) |
ExecStart 프로세스가 시작되는 즉시 | 포그라운드 데몬 |
exec |
바이너리 exec() 성공 시 | simple 보다 정확 |
forking |
ExecStart 가 종료하고 자식이 남을 때 | 전통적 fork 데몬. PIDFile= 필요 |
oneshot |
ExecStart 가 완전히 종료될 때 | 초기화 스크립트 |
notify |
서비스가 sd_notify(READY=1) 을 보낼 때 |
준비 완료를 직접 알리는 서비스 |
dbus |
D-Bus 이름 등록 시 | BusName= 필요 |
가장 흔한 실수가 Type=simple 인데 프로세스가 데몬화(fork 후 부모 종료)하는 경우다. systemd 는 부모가 죽는 것을 서비스 종료로 보고 실패 처리한다. 반대로 Type=forking 인데 포그라운드로 도는 프로그램이면 systemd 는 부모가 안 죽으니 영원히 기다린다 — 그러다 TimeoutStartSec 에 걸려 죽인다. "시작이 30초 멈추다 실패한다" 의 전형적인 원인이다.
프로덕션 권장은 Type=notify 다. 서비스가 실제로 요청을 처리할 준비가 된 시점을 정확히 알 수 있어 의존 서비스 시작이 안전해진다.
자주 나는 다섯 가지 실수
- ExecStart 에 셸 기능을 쓴다. 파이프, 리다이렉션, 변수 확장, 와일드카드는 동작하지 않는다. systemd 가 셸 없이 exec 하기 때문이다. 필요하면
ExecStart=/bin/bash -c '...'로 감싼다. - 상대 경로.
ExecStart=myapp은 실패한다. 절대 경로여야 한다. [Install]이 없다. 그러면systemctl enable이 아무것도 하지 않는다. 부팅 시 자동 시작이 안 된다.Restart=always+RestartSec=0. 크래시 루프가 시스템을 마비시킨다.StartLimitIntervalSec/StartLimitBurst로 상한을 둔다.Type=forking인데PIDFile=이 없다. systemd 가 메인 프로세스를 못 찾아 추적이 어긋난다.
보안 강화
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/myapp /var/log/myapp
ProtectSystem=strict 는 /usr, /boot, /etc 를 읽기 전용으로 만든다. 쓰기가 필요한 경로는 ReadWritePaths 로 예외를 준다. 이걸 켜고 나서 서비스가 안 뜨면 대개 쓰기 경로 누락이다.
타이머
cron 대신 쓸 수 있다. .timer 와 .service 가 짝이다.
# labhub-backup.timer
[Unit]
Description=Nightly backup
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
Unit=labhub-backup.service
[Install]
WantedBy=timers.target
Persistent=true 는 시스템이 꺼져 있어 놓친 실행을 부팅 후 즉시 수행한다. cron 에는 없는 기능이다.
현장에서 만나는 모습
systemd-analyze verify <유닛> 으로 문법과 참조를 검사할 수 있다. 배포 전에 이걸 돌리는 것이 좋은 습관이다.
다음 실습에서 할 것
유닛 파일을 처음부터 작성하고, 타이머 쌍을 만들고, 유닛 검증 스크립트를 직접 만든다. 채점기가 정상 유닛과 불량 유닛 양쪽으로 실행한다.