Fullmoon System

systemd 서비스 만들기: unit·보안·로그·재시작 검증

EdwardMoon

직접 설치한 프로그램을 부팅 때 시작하고, 실패했을 때 재시작하며, 실행 사용자와 로그를 일정하게 관리하려면 systemd 서비스를 등록한다. 이 글은 RHEL·Rocky Linux 8/9의 시스템 서비스 기준이다. 애플리케이션은 백그라운드로 분기하지 않고 전경에서 계속 실행되어야 한다.

systemd 서비스 만들기: Linux 컨테이너와 systemd 서비스의 시작, 상태, 로그, 네트워크와 복구 흐름
systemd 서비스 만들기에서 구성 요소, 보안 경계, 상태 확인과 복구 흐름

1. 실행 파일과 설정을 먼저 준비한다

예제의 /usr/local/bin/myapp/etc/myapp/config.yml은 실제 애플리케이션에 맞게 바꾼다. 아래 절차가 프로그램 자체를 설치하거나 설정 파일 내용을 만들어 주지는 않는다. 프로그램의 정상 실행 명령, 작업 디렉터리, 필요한 파일·포트와 정상 종료 방법을 먼저 확인한다. 기존 서비스에 적용한다면 변경할 unit과 설정 파일의 사본을 별도 경로에 보관한다.

command -v systemctl
systemd --version
sudo test -x /usr/local/bin/myapp
sudo test -f /etc/myapp/config.yml

검사에 실패하면 다음 단계로 넘어가지 않는다. 이미 패키지가 제공하는 서비스가 있다면 새 unit을 만들기 전에 systemctl cat 서비스명으로 확인하고 제공 unit의 drop-in을 사용하는 편이 유지보수에 유리하다.

2. 전용 계정과 쓰기 디렉터리를 만든다

getent passwd myapp
getent group myapp
# 계정이 없을 때 한 번만 실행한다.
sudo useradd --system --user-group --home-dir /var/lib/myapp --shell /sbin/nologin myapp
sudo install -d -o myapp -g myapp -m 0750 /var/lib/myapp
sudo chown root:myapp /etc/myapp/config.yml
sudo chmod 0640 /etc/myapp/config.yml
sudo -u myapp test -x /usr/local/bin/myapp
sudo -u myapp test -r /etc/myapp/config.yml

기존 myapp 계정이 있다면 용도와 기본 그룹을 확인하고 계정 생성 명령은 건너뛴다. 상위 디렉터리에도 접근 권한이 필요하므로 namei -l /etc/myapp/config.yml로 경로 전체를 확인한다. 실행 파일과 설정은 서비스 계정이 수정하지 못하게 하고, 애플리케이션이 쓰는 데이터 디렉터리만 해당 계정에 부여한다. SELinux가 활성화된 환경은 AVC 로그와 파일 컨텍스트를 함께 확인한다.

3. 서비스 unit을 작성한다

sudoedit /etc/systemd/system/myapp.service로 다음 내용을 저장한다.

[Unit]
Description=My application
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=60
StartLimitBurst=5

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/var/lib/myapp
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yml
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s
NoNewPrivileges=yes
PrivateTmp=yes
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
  • Type=simple은 시작한 프로세스를 서비스의 주 프로세스로 추적한다. 애플리케이션 준비 완료까지 보장하지 않으므로 별도 기능 검사가 필요하다.
  • ExecStart는 셸 명령줄이 아니다. &, 파이프와 리다이렉션을 그대로 넣지 않는다.
  • After는 순서, Wants는 함께 시작할 유닛 관계를 정한다. 네트워크 온라인 상태는 원격 DB나 API의 정상 응답까지 보장하지 않는다.
  • Restart=on-failure와 시작 횟수 제한을 함께 써서 빠른 실패 반복을 멈춘다. 정상 종료도 재시작할지는 프로그램 성격에 맞춰 결정한다.
  • PrivateTmp는 전용 임시 공간을 만든다. 다른 서비스와 /tmp 파일을 공유해야 하는 프로그램은 호환성을 확인한다.

비밀번호와 토큰을 unit의 Environment=에 직접 넣지 않는다. 애플리케이션이 지원하는 접근 제한 설정 파일이나 해당 systemd 버전이 지원하는 자격 증명 전달 기능을 사용한다.

4. 문법 확인 후 시작하고 실제 기능을 검사한다

sudo systemd-analyze verify /etc/systemd/system/myapp.service
# 위 검사에 오류가 없을 때 진행한다.
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl is-enabled myapp.service
systemctl is-active myapp.service
sudo systemctl status myapp.service --no-pager --full
sudo journalctl -u myapp.service -b -n 100 --no-pager

daemon-reload는 unit을 다시 읽고, enable은 부팅 시 시작 연결을 만들며, --now는 현재도 시작한다. 이미 실행 중인 서비스의 설정을 바꿨다면 프로그램이 지원하는 reload 또는 계획된 restart가 별도로 필요하다.

active만 확인하고 끝내지 않는다. 웹 서비스라면 해당 서비스의 실제 상태 확인 URL로 요청하고, 작업 처리기라면 시험 작업의 처리 결과를 확인한다. 유지보수 창에서 재부팅 후 시작 여부와 정상 종료도 시험한다. 예제의 종료 대기 30초는 작업 손실 없이 종료되는 시간을 기준으로 조정한다.

5. 자주 발생하는 실패를 구분한다

증상 확인할 내용
203/EXEC 실행 파일 경로, 실행 권한, 스크립트 인터프리터 및 SELinux 거부
200/CHDIR WorkingDirectory 존재와 서비스 계정의 디렉터리 접근 권한
217/USER User·Group 계정 존재 여부
시작 직후 종료 실제 인수·설정 오류, 프로그램이 daemon 모드로 분기하는지 여부
start-limit-hit 반복 실패 원인을 로그에서 수정한 뒤 제한 초기화와 재시작
sudo journalctl -u myapp.service --since '-10 minutes' --no-pager
# 원인을 고친 뒤 실행한다.
sudo systemctl reset-failed myapp.service
sudo systemctl restart myapp.service

변경을 되돌릴 때

새로 만든 서비스라면 sudo systemctl disable --now myapp.service로 중지와 부팅 등록을 해제한다. 기존 서비스 변경이라면 보관한 unit과 설정을 복원하고 daemon-reload 후 계획된 재시작·기능 검사를 한다. 서비스 계정이나 데이터 디렉터리는 의존성과 보존 필요를 확인하기 전 삭제하지 않는다.

공식 문서