본문 바로가기
IT Security/LINUX Basic

39. Linux - 나만의 Service를 만들어 구동해보자1

by Rosmary 2021. 9. 24.
728x90
반응형

 

 

 

 

 

필자가 지금까지 대부분의 Linux Service를 공부하며 포스팅 하다보니, "직접 서비스를 만들어 구동하는 것도 필자의 실력으로 이제는 가능하지 않을까?"라는 물음이 생겼다. 다행히 쉬는 날이 나름 길게 생긴 덕에 하루 동안 집중해서 하나씩 테스트를 하다보니, 직접 만든 쉘 스크립트로 서비스를 구동하는 것이 생각만큼 크게 어렵지는 않았다. 

 

이번 포스팅에서는 리눅스의 서비스 데몬, 그러나 사용자가 직접 만든 서비스를 구동하는 방법에 대해 알아보려한다. 포스팅에 앞서, 몇 가지 기초적인 지식이 필요한 내용이 있는데, 그 부분에 대해서 간략하게만 언급하고서 넘어가려 한다.

 

 

1. 쉘 스크립트(Shell Script)

 

이름이 거창해보이지만 어려운 개념은 아니다. 쉘 스크립트는 Linux의 명령어를 사용자가 원하는 순서대로 실행할 수 있도록 만든 파일이다. 예를 들어, 필자가 필자의 리눅스 시스템 점검을 진행한다고 하자. 메모리 사용량과 디스크 사용량, 특정 인터페이스 IP, 그리고 ntpd의 서비스 구동 여부를 명령어를 입력하여 확인해야 한다. 따라서 필자는 아래의 명령어를 순차로 입력하여 이 모든 것을 확인해야 한다.

 

-  Memory 사용량:  free -th

-  디스크 사용량:  df -h

-  인터페이스 IP :  ifconfig ens32

-  ntpd 서비스 구동 상태: systemctl status ntpd

 

 

지금이야 점검할 내용이 4개 밖에 안되니 그냥 명령어를 쳐서 확인하지, 점검 항목이 늘어날수록 이 많은 명령어를 하나씩 입력하면서 확인하기에는 시간도 상당히 낭비되니 좋은 방법은 아니다. 따라서 쉘 스크립트를 사용하면, 이 모든 내용을 한 번에, 그리고 조금 더 깔끔한 방식으로 화면에 출력할 수 있다. vi 편집기로, 확장자가 sh(쉘)인 파일을 열어, 아래의 내용을 넣은 뒤 저장하자.

포스팅을 위해 정말 대충 작성한 스크립트다. 참고만 하자.

 

스크립트의 실행은 sh, 또는 source 명령어로 실행하면 된다. 명령어 뒤에, 작성한 파일명을 명시해주면 된다. 오타가 없다면 아래와 같이 점검 내용이 깔끌하게 정리되어 나타나는 것을 확인할 수 있다.

 

 

위에서 든 예시대로, 쉘 스크립트는 사용자가 원하는 명령어를, 목적에 맞게 순서대로 정의한 파일이라고 보면 되며, 이 파일을 실행할 경우, 사용자의 목적에 맞는 명령어 결과가 자동으로 화면에 출력된다(물론, 잘 만들었다는 가정하에 말이다. 쉘 스크립트도 프로그래밍의 일종이라, 오류 없이 만들기 위해 부단히 많은 연습이 필요하다).

 

그럼, 쉘 스크립트는 서비스 구동 시, 어떤 역할을 하는지 궁금하신 분들이 있을 듯 하다. 쉘 스크립트는 서비스의 실행 또는 중지 시, 해당 서비스가 진행해야 할 일을 정의한, 일종의 작업 지시서라고 보면 된다. 

 

** 앞서 언급했듯이 쉘 스크립트는 일종의 프로그래밍이다. 따라서 쉘 스크립트만을 위해 포스팅을 하려면, 필자의 Python 포스팅처럼 별도의 카테고리를 생성하여 진행해야 한다. 아직은 쉘 스크립트 관련 포스팅을 진행할 계획이 잡혀있지 않는데(벌여놓은 것도 못 끝낸 마당이라...), 포스팅이 진행되면 이 포스팅에서 해당 카테고리로 링크를 걸어놓을 예정이다.  

 

** C나 Python 등으로 스크립트를 작성하여 동작하는 것도 가능하다. 하지만 Linux Service에 대해 설명하는 포스팅인만큼 다른 프로그램을 사용하는 것보다 쉘 스크립트만으로 설명을 진행하려 한다.

 

 

그럼, 이쯤에서 이런 질문이 생기는 분들이 있을 것이다.

 

"쉘 스크립트로 대부분의 작업을 자동으로 진행할 수 있다면, 굳이 쉘 스크립트를 서비스로 등록해서 사용하려는 이유는 무엇인가요? 그냥 sh 명령어로 쉘 스크립트를 백그라운드로 구동하게하면 되지 않나요?"

 

필자도 처음에는 그렇게 생각을 했다. 그러나 Linux를 사용하게 되면서 알게 된 것인데, 쉘 스크립트 실행의 단점 중 하나가, 반드시 쉘을 실행한 사용자가 쉘 스크립트 실행이 종료되는 순간까지 시스템에 로그인이 되어 있어야 한다는 것이다. 예를 들어, 1부터 20000까지의 숫자를 순차적으로 특정 파일에 작성하는 스크립트가 있다고 하자. 

 

1~20000 숫자를 print_number.txt라는 파일에 작성하는 스크립트다.

20000까지 숫자를 파일에 입력하는데 대략 30초 정도 걸린다. 스크립트가 정상 종료되어 생성된 파일을 열어보면, 20000까지 숫자가 제대로 입력된 것이 보인다. 하지만, 스크립트가 완전히 끝나지 않은 상태에서 putty 등 접속 프로그램을 종료하는 경우, 당연히 20000까지 결과가 출력되지 않는다.

 

쉘 스크립트를 백그라운드로 진행하더라도 접속 세션이 끊기면 스크립트도 중단되는 것이 확인된다.

 

 

물론, crontab으로 스케쥴을 걸어 계속 실행되도록 하는 것도 하나의 방법일 수 있지만, 이런 방식으로 여러 스크립트를 실행할 경우 쉘 스크립트의 중단, 재시작 등의 작업 및 관리가 어려워진다. 이 때문에 직접 만든 스크립트를 세심하게 관리할 필요가 있을 경우, 이를 서비스에 등록해서 사용하는 것이다.

 

 

 

 

2. service 확장자 파일.

 

service 확장자 파일은 Linux 서비스 데몬 구동을 어떻게 진행할 것인지를 정의한 파일이라 보면 된다. 쉘 스크립트가 서버 내 일련의 동작을 정의한다면, service 파일은 서비스 실행 시 어떤 쉘 스크립트 파일을 참고할 것인지, 서비스 실행 주체 ID를 어떤 계정으로 할 것인지, 이상이 생겼을 때 재기동은 어떻게 진행할 지 등을 정의한다. 

 

모든 Linux 서버에 기본으로 들어있는 sshd 서비스 파일을 확인해보자. CentOS7 기준으로, Linux에 설치된 모든 서비스 파일은 /usr/lib/systemd/system내에 존재한다. ls -lh로 해당 경로를 보면 sshd.service파일이 보일것인데, 이 파일을 cat으로 열어보자.

 

 

service 파일은 크게 세 부분으로 나누어져 있다. Unit, Service 그리고 Install. 각각의 부분에 들어가는 설정들은 마치 쉘 스크립트의 변수와 같은 형태를 띄고 있다. 구동할 서비스의 특성에 따라 각 변수에 적절한 값을 service 파일에 입력한 뒤, 저장해주면 된다. service 파일의 각 부분 및 변수에 대해 하나씩 알아보자. 내용이 많이 길다.

 

[Unit]

 

Unit은 구동할 서비스가 무엇인지, 서비스와 관련된 문서 등은 무엇인지를 나타내는, 일종의 초록과 같은 역할을 한다고 보면 된다. Unit에 사용되는 변수는 아래와 같이 6가지가 있다.

 

(1) Description 

Description은 해당 서비스가 무엇인지 나타낸다. Linux 서비스가 systemd에 의해 정상 구동, 중지되면, 정상 구동된 서비스의 로그가 systemctl status 명령어의 결과 하단에 출력되는데, Starting(Stopping) {Description 내용} 형태로 나타나게 된다.

 

만약 필자가 sshd.service 파일의 Description 값을 수정하면, 붉은 박스 안의 내용도 변경된 값으로 출력된다.

 

(2) Documentation

해당 서비스 데몬과 관련된 문서에 대해 작성하는 내용이다. sshd.service 파일의 경우, man 페이지에 대한 내용이 작성되어 있다. 하지만 인터넷의 URL 주소를 달아도 되며, Documentation 값이 설정되어 있지 않더라도 동작에 큰 문제는 없다.

 

 

(3) Before, After

Before 또는 After는 서버 부팅 과정 중, 본 서비스 동작 전 또는 동작 후에 구동되어야 하는 다른 서비스를 정의한다. sshd.service 파일을 보면 Before에 network.target과 ssh-keygen.service가 있는데, sshd 서비스가 구동된 후 Before에 정의된 서비스가 먼저 구동되도록 정의한다는 것이다. After는 반대로, 본 서비스 동작 전 구동될 서비스를 정의하는 것이다. 즉,

 

'After에 정의된 서비스 ->  본 서비스 -> Before에 정의된 서비스'

 

순으로 서비스가 시작된다는 의미다. 단, Before와 After의 설정 내용이 적용되려면, 본 서비스 및 설정값으로 지정된 서비스들이 반드시 부팅 시 자동으로 서비스가 실행(enable)되도록 선행 작업을 진행해야 한다. 서비스가 부팅 시 자동으로 시작하는지 여부는 아래의 명령어로 확인할 수 있다.

 

# 명령어:  systemctl is-enabled {서비스명}

 

해당 명령어의 결과가 enable로 출력되면 부팅 시 자동 동작하는 서비스라 보면 된다. 만약 disable로 뜬다면 아래의 명령어를 사용하여, 부팅 시 서비스가 자동으로 동작할 수 있도록 해주자.

 

# 명령어: systemctl enable {서비스명}

 

 

(4) Requires, Wants, Conflicts

Before와 After가 서버 부팅 시 서비스 구동 순서를 지정하는 설정이라면, Requires, Wants, Conflicts는 동시에 구동되는 서비스와 관련된 설정 내용이다. 먼저 Requires와 Wants의 경우, 본 서비스 동작 시, 동시에 동작해야하는 서비스를 정의한다. Conflicts는 반대로 본 서비스 동작 시, 동작하지 말아야 할 서비스에 대해 정의한다.

 

먼저 Requires와 관련하여 아래의 예시를 보자.

 

 

위의 파일은 필자가 직접 제작한 systemd_test라는 서비스 파일이다. 이 파일의 Unit 부분에 Requires=ntpd.service를 입력하고 저장한다. 현재 systemd_test와 ntpd 서비스는 동작하지 않는 상황이다. 그러나 필자가 systemd_test 서비스만 구동하더라도 ntpd 서비스가 자동으로 같이 시작되는 것을 확인할 수 있다. 아래의 결과를 보자.

 

 

Wants 역시 Require와 유사한 역할을 한다. 하지만 Requires와 다른 점은, Wants에 정의된 서비스는 어떠한 에러로 인해 동작하지 않더라도 본 서비스의 동작에는 영향을 끼치지 않는다는 것이다. Requires의 경우, 어떤 원인에 의해 정의된 서비스가 동작하지 않으면 본 서비스마저도 동작하지 않는다.

 

Conflicts는 Requires, Wants와는 반대의 기능을 한다. 만약 필자가 제작한 서비스가 ntpd와 conflic인 경우, ntpd 서비스가 실행중이면 본 서비스 실행 전 ntpd 서비스를 강제 중단하고 본 서비스를 실행한다.

 

systemd_test 서비스 실행 후, conflict의 서비스가 중단된 것이 보인다.

 

 

[Service]

[Unit]이 각 서비스의 초록 또는 개요와 같은 역할을 한다면, [Service]는 서비스 실행 방식, 시작 시 구동할 스크립트 또는 실행 파일 경로 등 서비스 구동과 직접적으로 연관되는 부분을 관리한다.

 

(1) Type

Type은 서비스의 구동 방식을 지정하는 변수다. 설정값으로 simple, forking, oneshot, dbus, notify, idle 6가지 값이 존재한다. 본 포스팅에서는 이들 중 자주 사용하는 옵션에 대해서만 언급하고 넘어가려 한다. 자세한 테스트 내용은 다음 포스팅을 참고하자.

 

-  simple

* 서비스 시작 시 실행할 쉘 스크립트 파일을 메인 프로세스로 지정한다.

* simple은 서비스 시작으로 메인 프로세스의 동작이 시작되는 순간, 서비스 구동에 관여하는 systemd가 해당 서비스의 시작 절차가 완료되었다고 판단하도록 하는 설정값이다.

-  forking

* 쉘 스크립트 실행 프로세스를 메인 프로세스로 지정하지 않고, 다른 프로세스 아래 부모 프로세스로 지정한다.

* simple과 달리, 서비스 시작으로 실행되는 쉘 스크립트가 종료되는 순간, 서비스 구동에 관여하는 systemd가 해당 서비스의 시작 절차가 완료되었다고 판단하도록 하는 설정값이다. 따라서, 서비스 구동 시 실행하는 쉘 스크립트 내에 백그라운드로 동작하는 다른 쉘 스크립트 또는 명령어가 있는 경우, forking으로 값을 설정하여 사용한다.

-  oneshot

* simple과 동일하게, 로 쉘 스크립트 실행 프로세스를 메인 프로세스로 지정한다.

* 그러나 forking과 동일하게, 서비스 시작으로 실행되는 쉘 스크립트가 종료되는 순간 서비스 구동에 관여하는 systemd가 해당 서비스의 시작 절차가 완료되었다고 판단하도록 하는 설정값이다.

* 다만 forking과는 다르게 쉘 스크립트 내부에 백그라운드로 분기하는 프로세스 없이도 동작이 가능하다

* 쉘 스크립트의 동작이 마무리되더라도 RemainAfterExit 설정값을 yes로 지정함으로써 해당 서비스가 실행 중인 상태로 표시할 수 있다. forking의 경우 쉘 스크립트가 종료되면 서비스도 자동 종료된다. 필자 생각에는 썩 좋은 옵션은 아닌 듯 한데 그래도 있는 설정이라 테스트를 해보았다.

 

!! 자료를 찾다보니 dbus, idle, notify는 simple과 매우 유사하게 동작하며 약간의 차이가 있다는 식으로 설명이 되어 있으나, 필자가 아무리 테스트해봐도 이들의 기능을 제대로 파악하기가 어렵다. 이 부분은 추후 확인되는대로 본 포스팅에 추가하려 한다.

 

 

(2) ExecStart, ExecStop, ExecReload

Exec으로 시작하는 옵션들은, 서비스의 시작(Start), 중지(Stop), 재시작(Reload) 시 실행할 쉘 스크립트 경로를 지정하는 설정들이다. 예를 들어, 필자가 /tmp/test/systemd_start1.sh 라는 파일에 일련 내용을 작성하여 저장한 뒤, 이 경로를 ExecStart 값에 넣게 되면 서비스 시작 명령어(systemctl start) 입력 시, ExecStart의 명령어가 실행되면서 서비스가 시작된다. ExecReload는 서비스 재기동(systemdctl restart), ExecStop은 서비스 중단(수동 중단 시에만 해당된다. systemctl stop) 시 해당 설정값으로 등록된 스크립트, 실행파일을 실행하도록 하는 옵션이다.

 

추가로, ExecStart의 경우 Type=oneshot으로 지정된 경우, ExecStartPre, ExecStartPost 옵션을 추가하여, ExecStart 전, 후로 실행할 스크립트를 지정하는 것도 가능하다. 이렇게 설정할 경우, 서비스 실행 시 ExecStartpre, ExecStart, ExecStartPost 순으로 지정된 스크립트가 동작하며 서비스가 시작된다.

 

(3) Restart

Restart 옵션은, 서비스가 Restart를 실행하는 상황에 대해 정의한 옵션이다. 설정값으로 no, on-failure, on-success, on-abort, always 등을 사용할 수 있다.

 

- no

* 서비스 재기동에 필요한 상황을 별도로 지정하지 않는 설정값이다.

- on-failure

* 서비스가 비정상적으로 종료되었을 때에 한하여 서비스를 재기동한다.

- on-success

* 서비스가 정상 종료되었을 경우에 한하여 서비스를 재기동한다. 리눅스에서는 프로세스 종료와 관련된 몇 가지 시그널들이 있는데, 이 중 SIGHUP, SIGINT,SIGTERM,SIGPIP 등의 시그널을 받거나, SuccessExitStatus 옵션에 정의된 시그널을 돌려받아 종료될 경우에 한해 정상 종료로 판단한다.

- on-abort

* SuccessExitStatus 또는 RestartPreventExitStatus 등에 정의되지 않은 종료 시그널이 탐지된 경우에만 재기동을 한다.

- always

* 서비스가 어떠한 방식으로 중단되더라도(root가 수동으로 강제 중지하더라도) 재기동하도록 하는 설정값이다.

 

(4) SuccessExitStatus, RestartPreventExitStatus

서비스 종료 시, 정상 시그널 및 예외 시그널을 정의하는 옵션이다. 만약 SuccessExitStatus="SIGHUP"으로 정의되어 있다면, 서비스 종료 시그널이 SIGHUP인 경우에 한해 정상 종료로 판단한다. RestartPreventExitStatus에 정의된 시그널이 종료 시 탐지되면 이들은 비정상 서비스 종료 시그널임에도 예외처리가 되어 서비스 재기동이 이루어지지 않게 된다.

 

(5)KillMode

KillMode는 서비스 중단 방법에 대해 정의하는 옵션이다.

- control-group

* 해당 서비스가 포함된 서비스 그룹 내 모든 서비스를 중단한다.

- process

* 해당 서비스의 메인 프로세스만 중단한다.

- none

*  기본값으로 서비스 종료 시, 별다른 액션을 취하지 않는다.

 

(6) User, Group

User와 Group은 서비스 실행에 관여하는 쉘 스크립트(또는 실행파일)를 실행할 주체를 결정한다. 예를 들어, User=test1로 지정된 경우, test1 계정이 서비스의 쉘 스크립트를 읽고 실행하게 된다. 그렇기 때문에 서비스 실행과 관련된 쉘 스크립트를 읽고 실행할 수 있는 권한이 test1에게도 부여가 되어 있어야 한다. Group도 동일한 개념으로 사용되나, 단일 계정이 아닌 계정 그룹에 적용된다는 차이점이 있다. 이 옵션들은 바로 아래에 서술할 PermissionsStartOnly 옵션과 함께 사용한다.

 

(7) PermissionsStartOnly

User, Group 등 특정 계정 및 계정 그룹에게만 서비스 실행 권한을 부여할 때 사용하는 옵션이다. yes, no로 설정값을 지정한다.

 

 

[INSTALL]

 

(1) Alias

Alias는 서비스 명을 다른 이름으로 사용할 수 있도록 하는 옵션이다. 단, 주의사항이 있는데, 반드시 서비스가 enable 상태로 부팅 시 자동 시작이 되도록 등록을 해주어야 한다는 것이다. 이 과정이 선행되지 않은 경우, 아무리 Alias에 지정된 값으로 systemctl start, status, stop을 시작해도 반응이 없게 된다. 

 

Alias 등록 시, {이름}.service 로 등록하면 되며, {이름}은 각자가 사용하기 편한 문자로 지정해주면 된다.

 

 

(2) WantedBy, RequestedBy

 

해당 서비스를 Enable로 전환할 경우, 이 서비스들이 포함될 서비스 그룹을 지정하는 옵션이다. 강한 의존성을 띄는 경우가 아니라면 WantedBy를 옵션으로 많이 사용한다. 

 

/etc/systemd/system 경로로 이동하면 여러 폴더들이 보이는데 각 폴더들이 리눅스 서비스 그룹을 나타낸다고 보면 된다. 우리가 일반적으로 사용하는 ntpd와 같은 서비스들은 보통 multi-user-target 그룹에 지정되어 있는데, ntpd가 enable로 설정되는 순간 ntpd.service 파일의 심볼릭 링크(바로가기)가 해당 폴더 안에 생성되게 된다. 서버 부팅 과정이 끝나면 multi-user.target 내의 서비스가 자동으로 실행되는데, 이 때 enable로 설정된 ntpd가 있다면 이 서비스도 multi-user.target에 포함되어 자동으로 실행되게 된다.

 

따라서, WantedBy의 설정값은, /etc/systemd/system 내의 폴더 명을 작성해주면 된다. 일반적으로 multi-user.target을 주로 사용한다.

 

 


 

이번 포스팅에서는 자체 Linux 서비스를 만들기 위해 필요한 개념 및 설정에 대해 간략하게(?) 알아보았다. 필자가 간략하게라고 언급한 이유는 다른게 아니라, 필자가 포스팅한 내용만으로 보았을 때 전체의 20%가 될까말까한 내용이기 때문이다. 추리고 추려서 꼭 필요한 부분만 넣으려고 했음에도...

 

원래는 하나의 포스팅에 모든 내용을 때려넣어 마무리하려했는데, 생각보다 작성해야 할 내용이 많아져서 어쩔 수 없이 테스트를 진행해야 하는 부분은 다음 포스팅에서 작성하려 한다. 실제 서비스의 동작에 대한 확인이 필요하신 분들은 다음 포스팅을 참고해주시기 바란다(언제 올릴 수 있을지는 모른다...)

 

 

 

Fin.

반응형

댓글