Systemd 유닛 이해하기: 서비스 설정 심층 분석
systemd 서비스 유닛의 작동 방식(Unit, Service, Install, 오버라이드, 재시작, 로그 포함)을 알아봅니다.
Systemd 유닛 이해하기: 서비스 설정 심층 분석
Systemd 유닛 파일은 서비스가 어떻게 시작되고, 어떤 의존성을 가지며, 어떤 사용자로 실행되고, 실패 시 어떻게 처리되는지를 결정하는 작은 텍스트 파일입니다. systemctl restart myapp.service가 어떤 앱에서는 작동하고 다른 앱에서는 작동하지 않는 이유가 궁금했다면, 그 답은 보통 유닛 파일에 있습니다.
이 가이드는 관리자와 개발자가 가장 자주 편집하는 .service 유닛에 초점을 맞춥니다. 동일한 시스템이 소켓, 타이머, 마운트, 장치, 경로 및 대상을 관리하지만, 운영상의 실수는 서비스 파일에서 가장 많이 발생합니다.
Systemd 유닛 파일이란?
Systemd 유닛 파일은 특정 유닛에 대한 구성 지시문을 포함하는 간단한 텍스트 파일입니다. 유닛은 systemd가 관리하는 리소스를 나타냅니다. 가장 일반적인 유형은 서비스 유닛으로, 백그라운드 프로세스나 애플리케이션을 시작, 중지, 재시작 및 관리하는 방법을 정의합니다.
유닛 파일은 각각 대괄호([])로 표시된 섹션으로 구성됩니다. 서비스 유닛의 가장 중요한 섹션은 다음과 같습니다:
[Unit]: 유닛에 대한 메타데이터, 의존성 및 순서를 포함합니다.[Service]: 실행 방법을 포함하여 서비스 자체의 동작을 정의합니다.[Install]: 유닛을 활성화 또는 비활성화하는 방법을 지정하며, 일반적으로 대상 유닛에 연결합니다.
Systemd는 여러 표준 디렉토리에서 유닛 파일을 찾습니다. 가장 일반적인 디렉토리는 다음과 같습니다:
/etc/systemd/system/: 로컬로 구성된 유닛의 경우, 기본값을 재정의합니다./usr/lib/systemd/system/: 많은 배포판에서 패키지로 설치된 유닛의 경우./lib/systemd/system/: 일부 Debian 계열 시스템에서 패키지 제공 유닛에 사용됩니다.
유닛을 검사해야 할 때는 경로를 추측하지 마십시오. 다음을 사용하십시오:
systemctl cat nginx.service
systemctl show -p FragmentPath nginx.service
systemctl cat은 기본 유닛과 드롭인 오버라이드를 모두 표시하므로 특히 유용합니다. 이것이 systemd가 실제로 사용하는 버전입니다.
.service 유닛 파일의 구조
일반적인 .service 유닛 파일을 분석하여 구성 요소를 이해해 보겠습니다.
[Unit] 섹션
이 섹션은 설명 정보를 제공하고 유닛 간의 관계를 정의합니다.
Description=: 서비스에 대한 사람이 읽을 수 있는 설명입니다.Documentation=: 서비스 문서에 대한 URL 또는 경로입니다.After=: 이 유닛이 나열된 유닛의 시작이 완료된 후에 시작되어야 함을 지정합니다.Requires=:After=와 유사하지만 나열된 유닛을 필수로 만듭니다. 필수 유닛이 시작에 실패하면 이 유닛도 실패합니다.Wants=: 더 약한 형태의 의존성입니다. 이 유닛은 원하는 유닛을 시작하려고 시도하지만, 해당 유닛의 실패가 이 유닛의 시작을 막지는 않습니다.Conflicts=: 이 유닛과 동시에 실행될 수 없는 유닛을 지정합니다.
[Unit] 섹션 예시:
[Unit]
Description=My Custom Web Server
Documentation=https://example.com/docs/my-web-server
After=network.target
이는 사용자 정의 웹 서버가 네트워크를 사용할 수 있게 된 후에 시작되어야 함을 나타냅니다.
한 가지 일반적인 함정: After=는 순서를 제어할 뿐, 요구 사항을 제어하지 않습니다. After=postgresql.service를 작성하면 systemd는 PostgreSQL과 이 서비스가 모두 트랜잭션의 일부일 때 PostgreSQL 이후에 서비스를 시작하지만, 자동으로 PostgreSQL을 가져오지는 않습니다. 앱이 동일한 트랜잭션에서 PostgreSQL을 시작해야 하는 경우 Wants=postgresql.service를 사용하거나, 하드 의존성의 경우 Requires=postgresql.service도 함께 사용하십시오.
그럼에도 불구하고 의존성은 상태 확인이 아닙니다. After=network.target은 DNS가 작동하거나, 원격 API에 연결할 수 있거나, 데이터베이스가 연결을 수락하고 있음을 보장하지 않습니다. 애플리케이션에는 여전히 합리적인 재시도 동작이 필요합니다.
[Service] 섹션
여기에 서비스 실행을 위한 핵심 로직이 있습니다.
Type=: 프로세스 시작 유형을 정의합니다. 일반적인 유형은 다음과 같습니다:simple(기본값):ExecStart=에 의해 시작된 프로세스가 메인 프로세스입니다. Systemd는ExecStart=프로세스가 포크된 직후 서비스가 시작된 것으로 간주합니다.forking: 자식 프로세스를 포크하고 종료하는 전통적인 데몬에 사용됩니다. Systemd는 부모 프로세스가 종료될 때까지 기다립니다.oneshot: 단일 명령을 실행하고 종료하는 작업에 사용됩니다.notify: 서비스가 시작이 완료되었을 때 systemd에 알림을 보냅니다.dbus: D-Bus 이름을 획득하는 서비스에 사용됩니다.
ExecStart=: 서비스를 시작하기 위해 실행할 명령입니다.ExecStop=: 서비스를 중지하기 위해 실행할 명령입니다.ExecReload=: 서비스를 다시 시작하지 않고 구성을 다시 로드하기 위해 실행할 명령입니다.Restart=: 서비스를 다시 시작해야 하는 시기를 정의합니다. 옵션에는no(기본값),on-success,on-failure,on-abnormal,on-watchdog,on-abort및always가 있습니다.RestartSec=: 서비스를 다시 시작하기 전에 대기할 시간입니다.User=/Group=: 서비스가 실행되어야 하는 사용자 및 그룹입니다.WorkingDirectory=: 실행된 프로세스의 작업 디렉토리입니다.Environment=/EnvironmentFile=: 서비스에 대한 환경 변수를 설정합니다.
[Service] 섹션 예시:
[Service]
Type=simple
ExecStart=/usr/local/bin/my-web-server --config /etc/my-web-server.conf
User=www-data
Group=www-data
Restart=on-failure
RestartSec=5
이 구성은 웹 서버를 시작하고, www-data 사용자 및 그룹으로 실행하며, 실패 시 5초 지연 후 자동으로 다시 시작합니다.
Type=은 특별히 주의해야 합니다. 많은 잘못된 유닛이 이전 init 스크립트가 데몬 모드를 사용했기 때문에 Type=forking을 사용합니다. 포그라운드에 남아 있는 최신 애플리케이션의 경우 Type=simple이 일반적으로 올바릅니다. 프로세스가 백그라운드로 포크되지만 systemd가 실제 메인 프로세스를 식별하는 방법을 알지 못하면 상태 보고 및 재시작이 오해의 소지가 있을 수 있습니다.
일회성 작업의 경우 Type=oneshot을 사용하고 완료된 작업이 활성 상태로 간주되어야 하는 경우 종종 RemainAfterExit=yes를 사용하십시오. 예를 들어, 방화벽 규칙을 준비하거나 특수 리소스를 마운트하는 유닛은 성공적으로 종료될 수 있지만 여전히 사용자가 신경 쓰는 상태를 나타냅니다.
[Install] 섹션
이 섹션은 유닛을 활성화하거나 비활성화할 때 사용됩니다. 유닛이 systemd의 대상 유닛과 어떻게 통합되는지 정의합니다.
WantedBy=: 활성화될 때 이 유닛을 "원하는" 대상(들)을 지정합니다. 부팅 시 시작되어야 하는 서비스의 경우multi-user.target이 일반적으로 사용됩니다.
[Install] 섹션 예시:
[Install]
WantedBy=multi-user.target
systemctl enable my-custom-service.service를 실행하면 systemd는 /etc/systemd/system/multi-user.target.wants/에서 서비스 파일로의 심볼릭 링크를 생성하여 시스템이 다중 사용자 런레벨에 도달할 때 서비스가 시작되도록 합니다.
유닛에 [Install] 섹션이 없어도 완전히 유효할 수 있습니다. 다른 설치 메커니즘이 존재하지 않는 한 systemctl enable로 직접 활성화할 수 없습니다. 일부 유닛은 수동으로 활성화하는 대신 의존성, 소켓, 타이머 또는 대상에 의해 풀인(pulled in)되도록 설계되었습니다.
사용자 정의 서비스 유닛 생성 및 관리
사용자 정의 서비스 유닛을 생성하는 과정을 살펴보겠습니다.
1단계: 유닛 파일 생성
/etc/systemd/system/ 디렉토리에 .service 확장자를 가진 새 파일을 만듭니다. 예를 들어 /etc/systemd/system/my-app.service를 생성해 보겠습니다.
[Unit]
Description=My Custom Application Service
After=network.target
[Service]
Type=simple
ExecStart=/opt/my-app/bin/run-app --port 8080
User=appuser
Group=appgroup
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
중요 고려 사항:
ExecStart명령이 액세스 가능하고 실행 권한이 있는 실행 가능한 스크립트 또는 바이너리를 가리키는지 확인하십시오.- 지정된
User및Group이 없으면 생성하십시오 (sudo useradd -r -s /bin/false appuser,sudo groupadd appgroup,sudo usermod -a -G appgroup appuser). - 지정된 명령을 사용하여 애플리케이션을 올바르게 시작하고 중지할 수 있는지 확인하십시오.
ExecStart=에 명령을 넣기 전에 가능하면 동일한 사용자로 수동으로 실행하십시오:
sudo -u appuser /opt/my-app/bin/run-app --port 8080
이렇게 하면 systemd가 개입하기 전에 누락된 실행 비트, 누락된 디렉토리, 잘못된 상대 경로 및 권한 문제를 발견할 수 있습니다. 수동으로 작동하면 유닛으로 이동하여 systemd가 감독을 처리하도록 하십시오.
2단계: Systemd 구성 다시 로드
유닛 파일을 생성하거나 수정한 후에는 systemd에 구성을 다시 로드하도록 지시해야 합니다.
sudo systemctl daemon-reload
이 명령은 새롭거나 변경된 유닛 파일을 스캔하고 systemd의 내부 상태를 업데이트합니다.
3단계: 서비스 활성화 및 시작
서비스를 즉시 시작하고 부팅 시 시작되도록 구성하려면:
sudo systemctl enable my-app.service # 부팅 시작을 위한 심볼릭 링크 생성
sudo systemctl start my-app.service # 지금 서비스 시작
4단계: 서비스 관리
systemctl 명령을 사용하여 서비스를 관리하십시오:
상태 확인:
sudo systemctl status my-app.service서비스가 활성 상태인지, 프로세스 ID, 최근 로그 항목 등을 표시합니다.
서비스 중지:
sudo systemctl stop my-app.service서비스 다시 시작:
sudo systemctl restart my-app.service서비스 다시 로드 (
ExecReload=가 정의된 경우):sudo systemctl reload my-app.service서비스 비활성화 (부팅 시 시작 방지):
sudo systemctl disable my-app.service
5단계: journalctl로 로그 보기
Systemd는 로깅을 위해 journald와 긴밀하게 통합됩니다. journalctl을 사용하여 서비스 로그를 볼 수 있습니다:
특정 서비스의 로그 보기:
sudo journalctl -u my-app.service실시간 로그 팔로우:
sudo journalctl -f -u my-app.service마지막 부팅 이후 로그 보기:
sudo journalctl -b -u my-app.service
모범 사례 및 팁
- 최신 애플리케이션에는
Type=notify사용: 애플리케이션이 지원하는 경우Type=notify는 systemd와 더 나은 통합을 제공하여 서비스 준비 상태를 정확하게 추적할 수 있습니다. - 루트가 아닌 사용자로 서비스 실행: 보안 위험을 최소화하기 위해
[Service]섹션에서 항상User=및Group=을 지정하십시오. - 의존성을 신중하게 정의:
After=,Requires=및Wants=를 사용하여 서비스가 올바른 순서로 시작되고 중요한 의존성이 충족되도록 하십시오. Restart=활용: 적절한 재시작 정책을 구성하여 서비스 가용성을 보장하십시오.- 유닛 파일을 단순하게 유지: 복잡한 시작 시퀀스의 경우 유닛 파일에 복잡한 명령을 직접 사용하는 대신
ExecStart=에서 호출하는 래퍼 스크립트 사용을 고려하십시오. systemctl cat <unit>사용: systemd가 보는 대로 유닛 파일의 전체 내용(오버라이드 포함)을 보려면 사용하십시오.systemctl edit <unit>사용: 이 명령은 기존 유닛에 대한 오버라이드 파일을 생성하는 편집기를 열며, 기본 유닛 파일을 직접 편집하는 것보다 더 깔끔한 방법입니다.
기존 유닛을 안전하게 편집
일회용 머신을 디버깅하는 경우가 아니라면 /usr/lib/systemd/system/ 또는 /lib/systemd/system/에 있는 패키지 소유 유닛을 편집하지 마십시오. 패키지 업그레이드가 해당 파일을 대체할 수 있습니다. 대신 오버라이드를 사용하십시오:
sudo systemctl edit nginx.service
그러면 /etc/systemd/system/nginx.service.d/ 아래에 드롭인이 생성됩니다. 예를 들어, 재시작 정책을 추가하려면:
[Service]
Restart=on-failure
RestartSec=5s
일부 지시문은 두 번 이상 지정할 수 있습니다. 다른 지시문은 교체 전에 지워야 합니다. ExecStart=가 전형적인 예입니다:
[Service]
ExecStart=
ExecStart=/usr/local/bin/my-nginx-wrapper
빈 ExecStart= 줄은 이전 값을 재설정합니다. 이것이 없으면 systemd가 유닛을 거부하거나 의도한 것보다 더 많은 명령을 유지할 수 있습니다.
유닛 또는 드롭인을 변경한 후에는 동일한 검토 루프를 사용하십시오:
sudo systemctl daemon-reload
systemctl cat my-app.service
sudo systemctl restart my-app.service
journalctl -u my-app.service -n 50 --no-pager
유닛 파일은 세 가지 작업을 분리하면 어렵지 않습니다. [Unit]은 관계를 설명하고, [Service]는 프로세스 동작을 설명하며, [Install]은 활성화를 설명합니다. 대부분의 실제 디버깅은 이러한 작업 중 어떤 것이 잘못된 가정으로 구성되었는지 찾는 것입니다.
실제 서비스 파일 연습
다음은 Python 웹 애플리케이션을 위한 작지만 실제적인 서비스입니다:
[Unit]
Description=Inventory API
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
Type=simple
User=inventory
Group=inventory
WorkingDirectory=/srv/inventory-api
EnvironmentFile=/etc/inventory-api/env
ExecStart=/srv/inventory-api/.venv/bin/gunicorn app:app --bind 127.0.0.1:9000
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
해당 파일에는 몇 가지 조용한 결정이 있습니다. 서비스는 루트가 아닌 inventory로 실행됩니다. 명령은 가상 환경의 gunicorn에 대한 절대 경로를 사용하므로 대화형 셸의 PATH에 의존하지 않습니다. 앱은 리버스 프록시가 공개적으로 노출할 것이므로 localhost에 바인딩됩니다. 환경 파일은 유닛 외부에 있으므로 배포 시 패키지 소유 서비스 메타데이터를 다시 작성하지 않고 구성을 업데이트할 수 있습니다.
의존성 라인은 의도적으로 적당합니다. After=postgresql.service는 PostgreSQL이 동일한 시작 트랜잭션의 일부인 경우 순서를 제어합니다. 데이터베이스가 연결 준비가 되었음을 증명하지 않으며 애플리케이션 재시도 로직을 대체하지 않습니다. network-online.target은 네트워크 준비 상태를 올바르게 구현하는 시스템에서 도움이 될 수 있지만 모든 원격 의존성에 연결할 수 있다는 보편적인 보장은 아닙니다.
이 서비스가 실패하면 첫 번째 확인은 예측 가능합니다:
systemctl status inventory-api.service
journalctl -u inventory-api.service -b --no-pager
systemctl cat inventory-api.service
sudo -u inventory /srv/inventory-api/.venv/bin/gunicorn app:app --bind 127.0.0.1:9000
마지막 명령은 프로덕션에서 실행 상태로 두는 것이 아닙니다. "구성된 사용자가 이 명령을 전혀 실행할 수 있습니까?"라고 묻는 진단 확인입니다. 앱을 가져오거나, 환경 파일을 읽거나, 로그 디렉토리에 쓸 수 없으면 systemd가 이를 해결해 주지 않습니다.
자주 보게 될 리소스 및 보안 지시문
많은 프로덕션 유닛에는 강화 또는 리소스 제어가 포함됩니다. 몇 가지 일반적인 예:
[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
MemoryMax=512M
CPUQuota=80%
이러한 지시문은 매우 유용할 수 있지만 가정을 깨뜨릴 수도 있습니다. PrivateTmp=true는 서비스에 개인 /tmp를 제공하므로 다른 프로세스가 거기에 쓴 파일을 볼 수 없습니다. ProtectHome=true는 /home, /root 및 /run/user에 대한 액세스를 차단할 수 있습니다. ProtectSystem=full은 서비스 관점에서 시스템의 많은 부분을 읽기 전용으로 만듭니다. 앱이 이전에 쓰던 곳에 갑자기 쓸 수 없는 경우 애플리케이션을 비난하기 전에 강화 설정을 검사하십시오.
리소스 제한에도 동일한 트레이드오프가 있습니다. MemoryMax=는 하나의 서비스가 전체 머신을 소비하는 것을 막을 수 있지만 값이 너무 낮으면 정상 부하에서 서비스가 종료될 수 있습니다. 저널에서 메모리 부족 메시지를 확인하고 제한을 높이거나 제거하기 전에 실제 사용량과 비교하십시오.
가장 유용한 디버깅 명령
서비스 유닛 작업 시 다음 명령을 가까이 두십시오:
systemctl status my-app.service
systemctl cat my-app.service
systemctl show my-app.service
systemd-analyze verify /etc/systemd/system/my-app.service
journalctl -u my-app.service -b --no-pager
systemctl show는 장황하지만 systemd가 유닛을 구문 분석한 후 계산한 속성을 노출합니다. 이는 기본값, 드롭인 또는 재설정 지시문에서 상속된 놀라운 값을 드러낼 수 있습니다. systemd-analyze verify는 서비스를 다시 시작하기 전에 일부 구문 및 의존성 오류를 포착합니다. 애플리케이션 테스트를 대체하지는 않지만 실행할 가치가 있을 만큼 충분한 실수를 포착합니다.