커스텀 Systemd 유닛 파일을 효과적으로 작성하고 관리하는 방법

이 포괄적인 가이드를 통해 커스텀 systemd 유닛 파일을 사용하여 Linux 서비스를 관리하는 기술을 마스터하세요. `.service` 파일을 생성, 구성 및 문제 해결하는 방법을 배우고, `ExecStart`, `WantedBy`, `Type`과 같은 중요한 지시문을 활용하세요. 이 문서는 단계별 지침과 실용적인 예제를 제공하여 애플리케이션 시작을 표준화하고 안정적인 작동을 보장하며 커스텀 프로세스를 Linux 시스템 환경에 원활하게 통합할 수 있도록 도와줍니다. 강력한 서비스 관리를 목표로 하는 개발자와 관리자에게 필수적입니다.

커스텀 Systemd 유닛 파일을 효과적으로 작성하고 관리하는 방법

커스텀 systemd 유닛 파일은 터미널에서 작동하는 명령을 운영 체제가 시작, 중지, 재시작, 로깅 및 감독할 수 있는 서비스로 변환합니다. 그 차이는 중요합니다. 셸의 명령은 사용자 환경을 상속받고 세션이 종료되면 종료됩니다. 반면 서비스는 명시적인 사용자, 작업 디렉터리, 재시작 정책, 종속성, 리소스 제한 및 로그를 갖습니다.

이것은 소규모 내부 API, 작업자, 사이드카 스크립트 및 일회성 데몬에 대해 제가 사용하는 실용적인 접근 방식입니다: 가장 간단한 올바른 유닛을 작성하고, 전용 사용자로 실행하며, 로그를 저널로 보내고, 실제 요구 사항이 발생할 때만 고급 지시문을 추가합니다.

Systemd 유닛 파일 이해하기

Systemd는 유닛으로 알려진 다양한 시스템 리소스를 관리하며, 이는 구성 파일로 정의됩니다. 이러한 유닛에는 서비스(.service), 마운트 지점(.mount), 장치(.device), 소켓(.socket) 등이 포함됩니다. 애플리케이션 및 백그라운드 프로세스를 관리하기 위해 .service 유닛 유형이 가장 일반적이고 관련이 있습니다.

Systemd 유닛 파일은 일반적으로 특정 디렉터리에 저장된 일반 텍스트 파일입니다. 우선 순위에 따른 기본 위치는 다음과 같습니다:

  • /etc/systemd/system/: 커스텀 유닛 파일 및 재정의를 위한 권장 위치입니다. 시스템 기본값보다 우선하며 시스템 업데이트 후에도 유지됩니다.
  • /run/systemd/system/: 런타임에 생성된 유닛 파일에 사용됩니다.
  • /usr/lib/systemd/system/: 설치된 패키지에서 제공하는 유닛 파일이 포함됩니다. 이 디렉터리의 파일을 직접 수정하지 마십시오.

커스텀 유닛 파일을 /etc/systemd/system/에 배치하면 systemd에서 제대로 인식되고 관리됩니다.

.service 유닛 파일의 구조

systemd .service 유닛 파일은 [SectionName]으로 표시된 여러 섹션으로 구성되며, 각 섹션에는 다양한 지시문(키-값 쌍)이 포함됩니다. 서비스 유닛의 세 가지 주요 섹션은 [Unit], [Service], [Install]입니다.

사용할 가장 중요한 지시문을 분석해 보겠습니다:

[Unit] 섹션

이 섹션에는 유닛에 대한 일반 옵션, 설명 및 종속성이 포함됩니다.

  • Description: 서비스를 설명하는 사람이 읽을 수 있는 문자열입니다. systemctl status 출력에 표시됩니다.
    Description=My Custom Python Web Application
    
  • Documentation: 서비스 문서를 가리키는 URL입니다(선택 사항).
    Documentation=https://example.com/docs/my-app
    
  • After: 이 유닛이 나열된 유닛 후에 시작되어야 함을 지정합니다. 시작 순서를 관리하는 데 도움이 됩니다. 웹 애플리케이션의 경우 네트워킹이 활성화되어 있는지 확인할 수 있습니다.
    After=network.target
    
  • Requires: 강한 종속성입니다. 필요한 유닛이 시작에 실패하면 이 유닛은 시작되지 않습니다. 필요한 유닛이 중지되면 이 유닛도 중지될 수 있습니다.
    Requires=docker.service
    
  • Wants: 약한 종속성입니다. 원하는 유닛이 실패하거나 찾을 수 없어도 이 유닛은 계속 시작을 시도합니다. 일반적으로 Requires보다 더 나은 기본값입니다.
    Wants=syslog.target
    

[Service] 섹션

이 섹션은 서비스의 실행 매개변수를 정의하며, 시작 및 중지 방법과 동작을 포함합니다.

  • Type: 프로세스 시작 유형을 정의합니다. systemd가 서비스를 모니터링하는 방식에 중요합니다.

    • simple (기본값): ExecStart 명령이 서비스의 메인 프로세스입니다. Systemd는 ExecStart가 호출된 직후 서비스가 시작된 것으로 간주합니다. 프로세스가 포그라운드에서 무기한 실행될 것으로 예상합니다.
    • forking: ExecStart 명령이 자식 프로세스를 포크하고 부모가 종료됩니다. Systemd는 부모 프로세스가 종료되면 서비스가 시작된 것으로 간주합니다. 애플리케이션이 자체적으로 데몬화되는 경우 사용합니다.
    • oneshot: ExecStart 명령은 작업이 완료되면 종료되는 일회성 프로세스입니다. 작업을 수행하고 종료되는 스크립트(예: 백업 스크립트)에 유용합니다.
    • notify: simple과 유사하지만 서비스가 준비되었을 때 systemd에 알립니다. 이를 위해서는 systemd 알림을 지원하는 애플리케이션이 필요합니다.
    • idle: ExecStart 명령은 모든 작업이 완료된 후에만 실행되어 시스템이 대부분 유휴 상태가 될 때까지 실행을 지연시킵니다.
    Type=simple
    
  • ExecStart: 서비스가 시작될 때 실행할 명령입니다. 이 섹션에서 가장 중요한 지시문입니다. 항상 실행 파일 또는 스크립트의 절대 경로를 사용하십시오.

    ExecStart=/usr/bin/python3 /opt/my_app/app.py
    
  • ExecStop: 서비스가 중지될 때 실행할 명령입니다(선택 사항). 지정하지 않으면 systemd가 프로세스에 SIGTERM을 보냅니다.

    ExecStop=/usr/bin/pkill -f 'my_app/app.py'
    
  • ExecReload: 서비스 구성을 다시 로드하기 위해 실행할 명령입니다(선택 사항).

    ExecReload=/bin/kill -HUP $MAINPID
    
  • User: 서비스 프로세스가 실행될 사용자 계정입니다. 보안에 필수적입니다. 반드시 필요한 경우가 아니면 root를 피하십시오.

    User=myappuser
    
  • Group: 서비스 프로세스가 실행될 그룹 계정입니다.

    Group=myappgroup
    
  • WorkingDirectory: 실행된 명령의 작업 디렉터리입니다.

    WorkingDirectory=/opt/my_app
    
  • Restart: 서비스를 자동으로 재시작해야 하는 시기를 정의합니다.

    • no (기본값): 재시작하지 않습니다.
    • on-success: 서비스가 정상적으로 종료된 경우에만 재시작합니다.
    • on-failure: 서비스가 0이 아닌 종료 코드로 종료되거나 신호에 의해 종료된 경우에만 재시작합니다.
    • always: 종료 상태에 관계없이 항상 서비스를 재시작합니다.
    Restart=on-failure
    
  • RestartSec: 서비스를 재시작하기 전에 대기할 시간입니다(예: 5s는 5초).

    RestartSec=5s
    
  • Environment: 실행된 명령에 대한 환경 변수를 설정합니다.

    Environment="APP_ENV=production" "DEBUG=false"
    
  • EnvironmentFile: 파일에서 환경 변수를 읽습니다. 각 줄은 KEY=VALUE 형식이어야 합니다.

    EnvironmentFile=/etc/default/my_app
    
  • LimitNOFILE: 서비스에 허용되는 최대 열린 파일 디스크립터 수를 설정합니다(예: 100000). 높은 동시성 애플리케이션에 중요합니다.

    LimitNOFILE=65536
    

[Install] 섹션

이 섹션은 서비스가 부팅 시 자동으로 시작되도록 활성화되는 방식을 정의합니다.

  • WantedBy: 이 서비스를 "원하는" 대상 유닛을 지정합니다. 대상 유닛이 활성화되면 이 서비스는 해당 대상의 .wants 디렉터리에 심볼릭 링크되어 대상과 함께 시작됩니다.
    • multi-user.target: 대부분의 서버 서비스에 대한 표준 대상으로, 비그래픽 다중 사용자 로그인이 있는 시스템을 나타냅니다.
    • graphical.target: 그래픽 환경이 필요한 서비스용입니다.
    WantedBy=multi-user.target
    
  • RequiredBy: WantedBy와 유사하지만 더 강한 종속성입니다. 대상이 활성화되면 이 유닛도 활성화되며, 이 유닛이 실패하면 대상도 실패합니다.

서버에서 백그라운드로 실행되는 대부분의 커스텀 서비스의 경우 Type=simpleWantedBy=multi-user.target이 올바른 시작점입니다. 애플리케이션이 이미 자체적으로 데몬화되어 있는 경우 해당 동작을 비활성화하거나 주의하여 Type=forking을 사용하십시오. 포그라운드 프로세스는 systemd가 감독하기 더 쉽습니다.

단계별 가이드: 커스텀 Systemd 서비스 생성 및 관리

실용적인 예제를 만들어 보겠습니다: 지정된 디렉터리에서 파일을 제공하는 간단한 Python HTTP 서버입니다. 이를 systemd 서비스로 설정하겠습니다.

1단계: 애플리케이션/스크립트 준비

먼저 애플리케이션 스크립트를 만듭니다. 이 예제에서는 간단한 Python HTTP 서버를 사용하겠습니다. 애플리케이션 디렉터리(예: /opt/my_app)를 만들고 그 안에 app.py를 배치합니다.

# /opt/my_app/app.py

import http.server
import socketserver
import os

PORT = int(os.environ.get("PORT", 8000))
DIRECTORY = os.environ.get("DIRECTORY", os.getcwd())

class Handler(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, directory=DIRECTORY, **kwargs)

print(f"Serving directory {DIRECTORY} on port {PORT}")

with socketserver.TCPServer(("", PORT), Handler) as httpd:
    print("Server started.")
    httpd.serve_forever()

디렉터리와 파일을 만듭니다:

sudo mkdir -p /opt/my_app
sudo nano /opt/my_app/app.py

(Python 코드를 붙여넣습니다)

스크립트를 실행 가능하게 만듭니다(python3 명령의 경우 선택 사항이지만 좋은 방법입니다):

sudo chmod +x /opt/my_app/app.py

보안을 위해 서비스 전용 사용자를 만드는 것을 고려하십시오:

sudo useradd --system --no-create-home myappuser

애플리케이션 디렉터리에 적절한 소유권을 설정합니다:

sudo chown -R myappuser:myappuser /opt/my_app

2단계: 유닛 파일 생성

이제 Python 애플리케이션에 대한 systemd 유닛 파일을 만듭니다. 이름을 my_app.service로 지정하겠습니다.

sudo nano /etc/systemd/system/my_app.service

다음 내용을 붙여넣습니다:

# /etc/systemd/system/my_app.service

[Unit]
Description=My Custom Python HTTP Server
Documentation=https://github.com/example/my_app
After=network.target

[Service]
Type=simple
User=myappuser
Group=myappuser
WorkingDirectory=/opt/my_app
Environment="PORT=8080" "DIRECTORY=/var/www/html"
ExecStart=/usr/bin/python3 /opt/my_app/app.py
Restart=on-failure
RestartSec=10s
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

참고: StandardOutput=journalStandardError=journal을 설정하여 서비스 출력을 systemd 저널로 보내 journalctl로 로그를 쉽게 볼 수 있도록 했습니다.

앱에 비밀이 필요한 경우 유닛 파일에 직접 넣지 마십시오. 제한적인 권한이 있는 환경 파일, 비밀 관리자 또는 배포별 자격 증명 지원을 사용하십시오. 유닛 파일은 예상보다 더 많은 사람이 읽을 수 있는 경우가 많습니다.

3단계: 유닛 파일 배치

지시된 대로 유닛 파일을 /etc/systemd/system/에 배치했습니다. 이것이 커스텀 유닛 파일이 있어야 할 위치입니다.

4단계: Systemd 데몬 다시 로드

유닛 파일을 생성하거나 수정한 후에는 systemd에 변경 사항을 알려야 합니다. 이는 systemd 데몬을 다시 로드하여 수행됩니다:

sudo systemctl daemon-reload

5단계: 서비스 시작

이제 서비스를 시작할 수 있습니다:

sudo systemctl start my_app.service

6단계: 서비스 상태 및 로그 확인

서비스가 올바르게 실행 중인지 확인합니다:

systemctl status my_app.service

예제 출력(일부 생략):

● my_app.service - My Custom Python HTTP Server
     Loaded: loaded (/etc/systemd/system/my_app.service; disabled; vendor preset: enabled)
     Active: active (running) since Tue 2023-10-26 10:30:00 UTC; 5s ago
       Docs: https://github.com/example/my_app
   Main PID: 12345 (python3)
      Tasks: 1 (limit: 1100)
     Memory: 6.5M
        CPU: 45ms
     CGroup: /system.slice/my_app.service
             └─12345 /usr/bin/python3 /opt/my_app/app.py

Oct 26 10:30:00 yourhostname python3[12345]: Serving directory /var/www/html on port 8080
Oct 26 10:30:00 yourhostname python3[12345]: Server started.

서비스 로그를 보려면 journalctl을 사용하십시오:

journalctl -u my_app.service -f

이 명령은 my_app.service의 로그를 표시하며 -f(follow)는 새 로그를 실시간으로 표시합니다.

브라우저 또는 curl을 사용하여 http://localhost:8080에서 서버를 테스트할 수도 있습니다(/var/www/html이 존재하고 일부 파일이 포함되어 있다고 가정).

7단계: 자동 시작을 위해 서비스 활성화

서비스가 시스템 부팅 시 자동으로 시작되도록 하려면 활성화해야 합니다:

sudo systemctl enable my_app.service

이 명령은 /etc/systemd/system/multi-user.target.wants/my_app.service에서 /etc/systemd/system/my_app.service로 심볼릭 링크를 만듭니다.

8단계: 서비스 중지 및 비활성화

실행 중인 서비스를 중지하려면:

sudo systemctl stop my_app.service

부팅 시 자동으로 시작되지 않도록 서비스를 비활성화하려면(수동으로 시작할 수 있도록 활성화 상태는 유지):

sudo systemctl disable my_app.service

서비스를 완전히 제거하려면 먼저 disable한 다음 stop하고 마지막으로 /etc/systemd/system/에서 .service 파일을 삭제하고 sudo systemctl daemon-reload를 실행하십시오.

9단계: 서비스 업데이트

app.py 스크립트 또는 my_app.service 유닛 파일을 수정하는 경우 systemd를 업데이트하고 서비스를 다시 시작해야 합니다:

  1. /opt/my_app/app.py 또는 /etc/systemd/system/my_app.service를 편집합니다.
  2. 유닛 파일을 수정한 경우 sudo systemctl daemon-reload를 실행합니다.
  3. 서비스를 다시 시작합니다: sudo systemctl restart my_app.service.

실제 서비스를 위한 더 안전한 패턴

작동하는 유닛이 항상 수년간 유지 관리하고 싶은 유닛인 것은 아닙니다. 이러한 패턴은 일반적인 실수를 방지합니다:

  • 포그라운드에서 실행합니다. systemd가 메인 프로세스를 감독하도록 합니다. ExecStart 내에서 nohup, screen, tmux, 백그라운드 & 또는 애플리케이션 데몬 모드를 피하십시오.
  • ExecStart를 직접 유지합니다. 파이프 또는 변수 확장과 같은 셸 기능이 필요한 경우 의도적으로 /bin/sh -c '...'를 호출하십시오. 그렇지 않으면 실행 파일을 직접 실행하십시오.
  • 전용 사용자를 사용합니다. /opt/my_app만 읽고 권한이 없는 포트에 바인딩하기만 하면 되는 서비스는 root로 실행되어서는 안 됩니다.
  • 유닛 편집 후 다시 로드합니다. 유닛 파일이 변경되면 sudo systemctl daemon-reload가 필요합니다.
  • 코드 배포와 유닛 변경을 분리합니다. Python 코드만 변경된 경우 서비스를 다시 시작하십시오. 유닛이 변경된 경우 먼저 systemd를 다시 로드하십시오.

새 유닛 문제 해결

서비스가 실패하면 다음부터 시작하십시오:

systemctl status my_app.service
journalctl -u my_app.service -n 100 --no-pager
systemctl cat my_app.service

일반적인 실패는 대개 명확합니다:

  • status=203/EXEC는 실행 파일 경로가 잘못되었거나, 파일이 없거나, 파일이 실행 가능하지 않음을 의미합니다.
  • Permission denied는 일반적으로 서비스 사용자가 파일을 읽거나, 디렉터리에 들어가거나, 로그를 쓰거나, 요청된 포트에 바인딩할 수 없음을 의미합니다.
  • address already in use는 다른 프로세스가 포트를 소유하고 있음을 의미합니다. sudo ss -tulpen | grep ':8080'으로 확인하십시오.
  • 수동으로 시작되지만 systemd에서 실패하는 서비스는 종종 환경 변수, 다른 작업 디렉터리 또는 홈 디렉터리의 파일에 의존합니다.

서비스 사용자로 명령을 테스트할 수 있습니다:

sudo -u myappuser /usr/bin/python3 /opt/my_app/app.py

이는 systemd의 환경을 완벽하게 재현하는 것은 아니지만 유닛 파일 세부 사항을 추적하기 전에 명백한 애플리케이션 오류를 잡아냅니다.

더 프로덕션 친화적인 변형

장기 실행 내부 서비스의 경우 일반적으로 몇 가지 안전 장치를 추가합니다:

[Unit]
Description=My Custom Python HTTP Server
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=myappuser
Group=myappuser
WorkingDirectory=/opt/my_app
EnvironmentFile=-/etc/my_app/my_app.env
ExecStart=/usr/bin/python3 /opt/my_app/app.py
Restart=on-failure
RestartSec=10s
TimeoutStopSec=30s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=/opt/my_app /var/log/my_app
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

ProtectSystem=full은 시스템의 많은 부분을 서비스에 대해 읽기 전용으로 만들므로 앱이 실제로 써야 하는 디렉터리에 대해서만 ReadWritePaths=를 추가하십시오. 한 번에 하나의 지시문씩 강화를 테스트하십시오. 보안 옵션은 유용하지만 구성을 읽거나 데이터를 쓸 수 없는 서비스는 시작 시 실패합니다.

모범 사례 및 문제 해결

  • 절대 경로: ExecStart, WorkingDirectory 및 유닛 파일 내의 다른 파일 경로에 항상 절대 경로를 사용하십시오. 상대 경로는 예기치 않은 동작을 초래할 수 있습니다.
  • 전용 사용자: 보안을 강화하고 침해 시 잠재적 피해를 제한하기 위해 권한이 없는 전용 사용자 계정(예: myappuser)으로 서비스를 실행하십시오.
  • 명확한 로깅: StandardOutput=journalStandardError=journal을 활용하여 서비스 출력을 systemd 저널로 보내십시오. journalctl -u <서비스_이름>을 사용하여 로그를 확인하십시오.
  • 종속성: After, WantsRequires를 신중하게 고려하여 서비스가 종속성(예: 네트워킹, 데이터베이스)에 대해 올바른 순서로 시작되도록 하십시오.
  • 변경 사항 테스트: 부팅 시 시작하도록 서비스를 활성화하기 전에 수동으로 시작 및 중지하여 철저히 테스트하십시오. 상태와 로그를 확인하십시오.
  • 리소스 제한: 서비스에 알려진 제한 또는 실패 모드가 있는 경우 LimitNOFILE, LimitNPROCMemoryMax와 같은 지시문을 사용하십시오.
  • 환경 변수: 유닛 파일이나 스크립트에 하드코딩하는 대신 변경되거나 환경 간에 달라질 수 있는 구성 값에 Environment= 또는 EnvironmentFile=을 사용하십시오.
  • 스크립트의 오류 처리: 애플리케이션 스크립트가 오류를 적절하게 처리하는지 확인하십시오. 0이 아닌 종료 코드는 Restart=on-failure를 트리거합니다.

경고: /usr/lib/systemd/system/의 유닛 파일을 직접 수정하지 마십시오. 변경 사항은 패키지 업데이트로 덮어쓰여질 가능성이 높습니다. 커스텀 유닛 또는 재정의에는 /etc/systemd/system/을 사용하십시오.

좋은 커스텀 유닛은 가장 좋은 의미에서 지루합니다: 명령이 명시적이고, 사용자가 권한이 없으며, 재시작 동작이 의도적이고, 로그를 쉽게 찾을 수 있습니다. 일단 그것이 견고해지면 systemd 타이머, 소켓 활성화 및 더 깊은 cgroup 제어가 자연스러운 다음 단계입니다.