LabHub
배우기 러닝패스 코스

RHEL-Family Administration

Writing a systemd Unit File Precisely

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

유닛 파일에서 가장 중요한 한 줄은 Type= 이다. systemd 가 "이 서비스가 언제 시작 완료됐는가" 를 판단하는 기준이기 때문이다.

Concept map: Type= · 패키지가 제공하는 유닛을 직접 고치지 마라. · After 와 Requires 는 다르다. · 순서

왜 이게 필요했나

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

AfterRequires 는 다르다. 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 다. 서비스가 실제로 요청을 처리할 준비가 된 시점을 정확히 알 수 있어 의존 서비스 시작이 안전해진다.

자주 나는 다섯 가지 실수

  1. ExecStart 에 셸 기능을 쓴다. 파이프, 리다이렉션, 변수 확장, 와일드카드는 동작하지 않는다. systemd 가 셸 없이 exec 하기 때문이다. 필요하면 ExecStart=/bin/bash -c '...' 로 감싼다.
  2. 상대 경로. ExecStart=myapp 은 실패한다. 절대 경로여야 한다.
  3. [Install] 이 없다. 그러면 systemctl enable 이 아무것도 하지 않는다. 부팅 시 자동 시작이 안 된다.
  4. Restart=always + RestartSec=0. 크래시 루프가 시스템을 마비시킨다. StartLimitIntervalSec/StartLimitBurst 로 상한을 둔다.
  5. 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 <유닛> 으로 문법과 참조를 검사할 수 있다. 배포 전에 이걸 돌리는 것이 좋은 습관이다.

다음 실습에서 할 것

유닛 파일을 처음부터 작성하고, 타이머 쌍을 만들고, 유닛 검증 스크립트를 직접 만든다. 채점기가 정상 유닛과 불량 유닛 양쪽으로 실행한다.