LabHub

RHEL 계열 관리 · 서비스 유닛 파일 작성 · 이론

systemd 유닛 파일을 정확히 쓰기

LabHub 에서 이어서 보기

한 줄 요약

유닛 파일에서 가장 중요한 한 줄은 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 ApplicationDocumentation=https://wiki.internal/myappAfter=network-online.target postgresql.serviceWants=network-online.targetRequires=postgresql.serviceConditionPathExists=/etc/myapp/config.yaml[Service]Type=notifyUser=myappGroup=myappWorkingDirectory=/opt/myappEnvironmentFile=-/etc/myapp/envExecStartPre=/opt/myapp/bin/check-config --validateExecStart=/opt/myapp/bin/server --config /etc/myapp/config.yamlExecReload=/bin/kill -HUP $MAINPIDRestart=on-failureRestartSec=5StartLimitIntervalSec=300StartLimitBurst=5TimeoutStartSec=30StandardOutput=journalSyslogIdentifier=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=trueProtectSystem=strictProtectHome=truePrivateTmp=trueReadWritePaths=/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:00Persistent=trueUnit=labhub-backup.service[Install]WantedBy=timers.target

Persistent=true시스템이 꺼져 있어 놓친 실행을 부팅 후 즉시 수행한다. cron 에는 없는 기능이다.

현장에서 만나는 모습

systemd-analyze verify <유닛> 으로 문법과 참조를 검사할 수 있다. 배포 전에 이걸 돌리는 것이 좋은 습관이다.

다음 실습에서 할 것

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