RHEL 7系のCentOS 7などではそれまでRHEL 6系で使われていたSystem V系のinitから Systemdを用いたデーモン管理がベースになるようになりました。
CentOS 7でデーモンを自作したいものがあって作ったので 基本的な作り方についてまとめておきたいと思います。
- Systemd (systemctl)
- デーモン本体作成
- 最小限の設定
- サービスファイル
- rsyslogの設定ファイル
- logrotate
- インストール/アンインストールスクリプト
- 動作チェック
- まとめ
Systemd (systemctl)
initのときには/etc/init.d/の中にデーモン名の(通常)シェルスクリプトが入っていて、
このスクリプトがstartとかstopとかの引数を受ける様に作られ、
直接
# /etc/init.d/httpd start
とかするか、
# service httpd start
のようなservice <daemon> <command>と言った形でデーモンを管理していました。
serviceというコマンドを使いますが、実態は単にinit.dにあるスクリプトを呼ぶラッパーの様なものでした。
また、OS起動時に走らせる様にするためにはchkconfigというコマンドなど、serviceとは別のコマンドで管理されていました。
chkconfig用の設定みたいなものもデーモン用スクリプトに書いておく必要があったりもしました。
なので色々なものがごちゃごちゃしていてデーモンを作るのは結構面倒だったイメージがあります。
これがSystemdではこれで全てを一括管理し、さらにはデーモン用ファイルも いわゆる設定ファイルの様な簡単な記述をするだけで良いようになりました。
Systemdでは/usr/lib/systemd/system/や/etc/systemd/system/と言ったディレクトリに
<daemon.service>みたいな名前のファイルを置いて管理します。
これらのディレクトリに同じデーモンがある場合には後者の方が優先されます。
yumなどでインストールされる際には前者に入れられ、
ユーザーが変更したりしたい場合には後者のディレクトリにコピーして使ったりすることが想定されています。
ファイルの内容はinitの場合と全く変わって独自のフォーマットで いろいろと指定するような形になっています。
繰り返しになりますが、initのときには基本的に全ての動作をシェルスクリプトなどで自分で書く必要がありましたが、 Systemctlでは簡単な記述でデーモンに登録できるようになっています。
initの場合には/etc/init.d/httpd startみたいに直接呼んでもservice httpd startと
同じ動作をしましたが、
Systemctlでは/usr/lib/systemd/system/httpdなどのファイル自体はスクリプトではないので直接は呼べません。
# systemctl start httpd
の様にsystemctlコマンドを通じて管理することになります。
serviceのときとデーモン名とstartなどのコマンドの順番が逆なので未だに時々どっちだかわからなくなりますが、
systemctlではGitなどの様にサブコマンドを呼んでデーモン名を引数として渡す
# systemctl <subcommand> <daemon>
いった感じになっています。
(initの場合はスクリプトへのラッパー的な感じなので、最後のstartとかがデーモン名スクリプトへの引数、といった雰囲気で
先にデーモン名が来てた感じです。)
systemctlの基本的なサブコマンドとしては
- start: デーモン開始
- stop: デーモン停止
- restart: デーモンリスタート
- is-active: デーモンが走っているかどうかの確認
- enable: デーモンをOS開始時にスタートする様にする
- disable: デーモンをOS開始時にスタートしない様にする
- is-enabled: デーモンがOS開始時にスタートするようになっているかどうかの確認
など。
initと違いOS起動時の登録なども基本的に全てsystemctlコマンドで管理できる様になっています。
デーモン本体作成
簡単なデーモンを作って登録してみようと思います。
| 1 2 3 4 5 6 7 8 |  | 
こんな感じのただひたすら10秒毎にHello world!を言うだけのスクリプト。
これをデーモンとして登録し、ログファイルなども管理してみます。
最小限の設定
単にデーモンとして動かしたいだけなら
| 1 2 3 4 5 6 7 8 9 |  | 
というサービスファイルを作ってデーモン本体とこのサービスファイルを設置して
# systemctl enable hello_world
# systemctl start hello_world
とするだけでOKです(起動時に走らせる必要がなければstartだけでOK。)
ログはjournalctl -u hello_world.serviceで確認出来ます。
サービスファイル
ログファイルを書き出したりしたいのでもうちょっと色々やってみます。
以下の様な内容でサービスファイルを作ります。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 |  | 
これら以外にもたくさん設定項目があって出来ることもたくさんありますが、 とりあえず載っているものだけ説明しておくと
- Unit- Description: デーモンの説明
 
- Service- ExecStart: デーモン開始のコマンド
- ExecStop: デーモン停止のコマンド- 今回のスクリプトは自分で止まる手段を持たないのでkillする。
- この際、MAINPIDという値がExecStartで開始されたプログラムのPIDを持っていてこれを使って上の様にkillすることができる。
- この様な簡単な単独コマンドであれば/var/run/<daemon>.pidみたいなPIDファイルを使わずに管理できる。
 
- 今回のスクリプトは自分で止まる手段を持たないので
- Restart:- alwaysにしておくと何らかの理由で落ちた場合(手動で- killした場合なども)自動でリスタートする。- noにすればしない。(他にもいくつかオプションあり。)
- StandardOutput: 標準出力の出力先
- StandardError: 標準エラーの出力先
- SyslogIdentifier: syslogでこの名前で認識される
 
- Install- WantedBy:- enableしたときにどの状態ならスタートするかを決めるターゲットを決める。- multi-user.targetがいわゆるGUIなしで普通に立ち上げたときに対応するので通常はこれを指定しておけばOK。
 
デーモンとして走らせるのに特に重要なのはExecStartとExecStopの部分です。
Systemdによってこんな感じでコマンドを指定するだけでスタートできますし、
停止も$MAINPIDという便利な変数を使って簡単にkillする設定を作れます。
その下、StandardOutputなどでsyslogを指定していますが、
デフォルトはjournalです。
このjournalを指定すると
$ journalctl -u hello_world.service
などでアクセスできるジャーナルに書き込まれます。
systemsctl status hello_worldなどで表示されるログもこれの一部。
journalctlを使うと時間をしていしてログを抜き出したりすることが簡単にできるので
これを上手く使えるといろいろ便利なのですが、
今回は普通にファイルに書き出したいのでsyslogを指定しています。
使った環境にはrsyslogがインストールされていて、syslogを指定するとrsyslogに渡されます。
単純に
ExecStart = /usr/bin/hello_world >/var/log/hello_world.log 2>&1
みたいなことをしてもコマンドを実行する際の出力が先にStandardOutputなどで指定されたものに指定されるので
上手くいきません。
ここで指定したいなら
ExecStart = /bin/sh -c 'exec /usr/bin/hello_world >/var/log/hello_world.log 2>&1'
の様にシェルの中でコマンドを実行してそこで出力、的なことをする必要があります。
シンプルにやるのであればこれでも十分だとは思います。
もしくはコマンドの中でファイルに出力するようにしておけばもちろんそれはそのままファイルに出力されますが。
今回はサービスファイルをきれいに保ってログは別途管理する、ということを考えて
syslogを使ってみます。
ただし、syslogを指定した場合でもログはjournalにも同時に流れる様になっています。
ここからもjournalを使うのが主流になっていく感もあります。
一方、やはり直接ファイルに書き出したい、と考える人も多いようで、
昨年末位にこれに関するコミットがマージされて
最新版ではfile:...とすることで直接ファイルへの書き出しをできるようになっています。
(v236から。今入ってるのはv219だった。)
Support pointing StandardInput/StandardOutput/StandardError to file · Issue #3991 · systemd/systemd
まだCentOS 7などの標準では使えませんが
使える様になったらこれを使うのもありなのかも。
(少なくともsh -cとかして無理やりやるよりはきれい。)
最後のWantedByで決めるターゲットとしては以下の様なレベルがあります。
| ランレベル | ターゲット | 説明 | 
|---|---|---|
| 0 | poweroff.target | システム停止 | 
| 1 | rescue.target | レスキューシェル(シングルユーザーモード) | 
| 2(,3,4) | multi-user.target | マルチユーザーモード | 
| 5 | graphical.target | マルチユーザーモード+GUI | 
| 6 | reboot.target | 再起動 | 
通常multi-user.targetで良いと思います。
GUIを使うためだけのデーモンなどがもしあればgraphical.targetなど。
rsyslogの設定ファイル
rsyslogに送ったログは、そのまま何もしなければ/var/log/messagesに書き出されます。
これを変更するための各プログラムに対する設定ファイルは/etc/rsyslog.dの下に入っています。
ここに次の様なファイルを用意します。
| 1 2 |  | 
ここでprogramnameは先程
SyslogIdentifierで指定した名前を指定してそれと同じなら
/var/log/hello_world.logに書き出す、という設定にします。
次の& stopでこの出力を他に出さない、つまり/var/log/messagesには出力しない、という設定になります。
古いシステムで& ~としているものもあったりしますが、これだと最近の環境だとwarningをだす環境もあるので
stopの方が良いです。
ちなみにこれで書き出されるログには
<date> <hostname> <processname>[<pid>] logoutput...
といった感じに時間情報などが追加された状態になります。
もともとのログ出力に時間情報などが全く無い場合には便利ですが、 逆にあると邪魔かもしれません。
その場合は出力を考え直すか、 もしくは上に書いた例の様に直接サービスファイルの中の設定でログに 書き出す様にした方が良い場合もあると思います。
logrotate
これでログまで思い通りに書き出せる様になったわけですが、 せっかくなのでちゃんとしたデーモンになるためにログをローテションできる様にしたいと思います。
ログが大きくなったり古くなったりしたらlog.1とかlog-20180910とかに移して
新たなlogに書き出していく、ということ。
これにはlogrotateがインストールされて動いている必要があります。
logrotateでは/etc/logrotate.d/に各ログファイルの細かい設定などを書くことができます。
| 1 2 3 4 5 6 7 8 9 10 11 12 |  | 
- missingok: ログがなくてもエラーを出さない。
- rotate: この数だけログが保存される。
- dateext: hello_world.log-20180910の様な日付のついたローテーションファイルを作る。これがないとhello_world.log.1とか数字の着くローテーションファイルになる。
- delaycompress:- gzipで圧縮する。ファイルはhello_world.log-20180910.gzの様になる。- compressだとすぐに圧縮、- delaycompressだと最新の一つは圧縮せずに通常ファイルのままにしておく。- プロセスがファイルに出力を続けていると変更された元のファイルに書き出し続けることがある場合、テキストファイルのママにしておかないと一部情報が失われてしまう。
 
- daily: 1日毎にローテーションを実行。
- minsize: このサイズ以下ならローテーションしない。- 100Mで100MB、- 100kなら100kB。
- postrotate~- endscript: ローテーション後に実行するコマンド。
これで、ファイルが100MB以上になったら1日一回のチェックタイムのときに hello_world.logがhello_world.log-20180910に変更され、 それ以前のhello_world.log-20180909があればhello_world.log-20180909.gzと圧縮されます。
さらに古いファイルが10個以上になったら一番古いファイルが削除されます。
このとき、rsyslogもhello_worldも変更を知らないので
元のファイル(hello_world.log-20180910)に書き出し続けます。
そこでpostrotateを使ってrsyslogとhello_worldのデーモンを再起動しています。
これで再び/var/log/hello_world.logに書き出される様になります。
rsyslogの再起動に関しては、/etc/logrotate.d/syslogに
postrotate
    /bin/kill -HUP `cat /var/run/syslogd.pid 2> /dev/null` 2> /dev/null || true
endscript
という記述があって、PIDファイルを使ってPIDを直接killする様になっています。
/usr/lib/systemd/system/rsyslog.serviceを見ると
Restart=on-failure
という記述があり、正常に終了しなかった場合にはリスタートするようになっていて、
killされたときなどは自動で再起動するのでlogrotateでkillして
再起動する様にしています。
systemctlを使ったほうが良さげな感じですが、
システムによってはsystemctlでかなりしてなかったりもするので
他のプログラムでもこの様なPIDファイルを使った記述が結構あります。
また、rsyslogのバージョン?によっては/var/run/rsyslogd.pidを使う場合もあるようで、
postrotate
  /bin/kill -HUP `cat /var/run/syslogd.pid 2> /dev/null` 2> /dev/null || true
  /bin/kill -HUP `cat /var/run/rsyslogd.pid 2> /dev/null` 2> /dev/null || true
endscript
と2つ書いてあるものもあります。
今回はhello_world自体がPIDを使ってkillすることが面倒なので
systemctlがある環境専用になりますが上の様な形で書いています。
インストール/アンインストールスクリプト
上のテストデーモンのスクリプトや設定ファイルを以下のレポジトリに置きました。
これをcloneして簡単にインストール出来るようにしてみました。
# https://github.com/rcmdnk/systemctl-hello-world.git
# cd systemctl-hello-world
# ./install.sh
で上のスクリプトや設定ファイルが入れられます。
uninstall.shでそれらのファイルを削除します。
今回のデーモンはシェルスクリプトなんでコンパイルとかも必要ないので 実行ファイルを置いて、上で設定した各種設定ファイルを置くだけです。
アンインストールも置いたものを削除するだけ。
インストール/アンインストールスクリプトの内容は以下の様な感じです。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 |  | 
| 1 2 3 4 |  | 
動作チェック
フィアルを置いてただけですが、ちゃんとsystemctl statusで確認でき、systemctl start、systemctl stopで起動/停止出来ることを確認します。
それからログファイルがちゃんと/var/log/hello_world.logに書かれ、かつ/va/log/messagesには出てないことを確認。
上にも書いたようにsyslogに書き出すとjournalからも参照出来る様になるのでそれも確認。
# systemctl status hello_world
● hello_world.service - Hello world daemon
   Loaded: loaded (/usr/lib/systemd/system/hello_world.service; disabled; vendor preset: disabled)
   Active: inactive (dead)
# systemctl is-active hello_world
inactive
# systemctl start hello_world
# systemctl status hello_world
● hello_world.service - Hello world daemon
   Loaded: loaded (/usr/lib/systemd/system/hello_world.service; disabled; vendor preset: disabled)
   Active: active (running) since Mon 2018-XX-XX XX:XX:XX UTC; 2s ago
 Main PID: 29545 (hello_world)
   CGroup: /system.slice/hello_world.service
           ├─29545 /bin/sh /usr/bin/hello_world
           └─29546 sleep 10
Sep XX XX:XX:XX example_host systemd[1]: Started Hello world daemon.
Sep XX XX:XX:XX example_host systemd[1]: Starting Hello world daemon...
Sep XX XX:XX:XX example_host hello_world[29545]: Starting Hello World...
# systemctl is-active hello_world
active
# journalctl -u  hello_world.service
-- Logs begin at Sun 2018-XX-XX XX:XX:XX UTC, end at Mon 2018-XX-XX XX:XX:XX UTC. --
Sep XX XX:XX:XX example_host systemd[1]: Started Hello world daemon.
Sep XX XX:XX:XX example_host systemd[1]: Starting Hello world daemon...
Sep XX XX:XX:XX example_host hello_world[29545]: Starting Hello World...
Sep XX XX:XX:XX example_host hello_world[29545]: Starting Hello World...
...
# cat /var/log/hello_world.log
Sep XX XX:XX:XX example_host hello_world: Starting Hello World...
Sep XX XX:XX:XX example_host hello_world: Hello world!
Sep XX XX:XX:XX example_host hello_world: Hello world!
...
# grep "hello_world" /var/log/messages
#
# systemctl status hello_world
● hello_world.service - Hello world daemon
   Loaded: loaded (/usr/lib/systemd/system/hello_world.service; disabled; vendor preset: disabled)
   Active: inactive (dead)
Sep XX XX:XX:XX example_host hello_world[29545]: Starting Hello World...
Sep XX XX:XX:XX example_host hello_world[29545]: Starting Hello World...
...
Sep XX XX:XX:XX gcp-wn-1core-09-template systemd[1]: Stopping Hello world daemon.
Sep XX XX:XX:XX gcp-wn-1core-09-template hello_world[29644]: 29645
Sep XX XX:XX:XX gcp-wn-1core-09-template systemd[1]: Stopped Hello world daemon.
このままだと再起動時には起動されませんが、起動するようにするには
# systemctl is-enabled hello_world
disabled
# systemctl enable hello_world
Created symlink from /etc/systemd/system/multi-user.target.wants/hello_world.service to /usr/lib/systemd/system/hello_world.service.
# systemctl is-enabled hello_world
enabled
と、is-enabledでenabledになってればOK。
logrotateも確認してみます。
# systemctl start hello_world
# logrotate -d /etc/logrotate.d/hello_world.conf
reading config file /etc/logrotate.d/hello_world.conf
Allocating hash table for state file, size 15360 B
Handling 1 logs
rotating pattern: /var/log/hello_world.log  after 1 days (10 rotations)
empty log files are rotated, only log files >= 104857600 bytes are rotated, old logs are removed
considering log /var/log/hello_world.log
  log does not need rotating (log has been already rotated)
#
logrotate -dで実際にローテーションを行わないデバッグモードでテストすることが出来ます。
ここでは設定項目によってどの様な動作が行われてるか、というのが書いてあってdailyだとか10 rotations、100M(104857600 bytes)とかが
正しく反映されてる事が確認できます。
最後のメッセージですでにローテション済というメッセージが出てますが、 これは一回もローテーションを行ってないと前回の実行時間の記録がなく、その場合は期間設定では引っかからない仕様になってるためです。
前回実行時間は/var/lib/logrotate/logrotate.statusにあります。
# cat  /var/lib/logrotate/logrotate.status
logrotate state -- version 2
"/var/log/yum.log" 2018-9-09-17:0:0
"/var/log/boot.log" 2018-9-09-17:0:0
"/var/log/chrony/*.log" 2018-9-9-17:0:0
...
一度hello_worldについて実行してみると
# logrotate -d /etc/logrotate.d/hello_world.conf
# cat /var/lib/logrotate/logrotate.status
logrotate state -- version 2
"/var/log/yum.log" 2018-9-9-17:0:0
"/var/log/boot.log" 2018-9-9-17:0:0
"/var/log/hello_world.log" 2018-9-10-1:0:0
"/var/log/chrony/*.log" 2018-9-9-17:0:0
...
とhello_world.logの情報が加わります。
このままだとまた1日経ってないことになるので、このファイルを書き換えます。
| 1 2 |  | 
これでデバッグしてみると
 # logrotate -d  /etc/logrotate.d/hello_world.conf
 reading config file /etc/logrotate.d/hello_world.conf
 Allocating hash table for state file, size 15360 B
 Handling 1 logs
 rotating pattern: /var/log/hello_world.log  after 1 days (10 rotations)
 empty log files are rotated, only log files >= 104857600 bytes are rotated, old logs are removed
 considering log /var/log/hello_world.log
   log does not need rotating ('misinze' directive is used and the log size is smaller than the minsize value
と言った感じで今度はminsizeよりも大きいからまだやる必要ない、と出ます。
ということでminsizeを書き換えてみます。
| 1 2 |  | 
これで
# logrotate -d  /etc/logrotate.d/hello_world.conf
reading config file /etc/logrotate.d/hello_world.conf
Allocating hash table for state file, size 15360 B
Handling 1 logs
rotating pattern: /var/log/hello_world.log  after 1 days (10 rotations)
empty log files are rotated, only log files >= 1024 bytes are rotated, old logs are removed
considering log /var/log/hello_world.log
  log needs rotating
rotating log /var/log/hello_world.log, log->rotateCount is 10
dateext suffix '-20180910'
glob pattern '-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'
glob finding old rotated logs failed
fscreate context set to system_u:object_r:var_log_t:s0
renaming /var/log/hello_world.log to /var/log/hello_world.log-20180910
running postrotate script
running script with arg /var/log/hello_world.log: "
      systemctl restart rsyslog
      systemctl restart hello_world
"
と、こんな感じにローテーションが必要で-20180910というのでローテーションします、と出てきます。
ここで実際にやってみると
# logrotate /etc/logrotate.d/hello_world.conf
# ls /var/log/hello_world*
/var/log/hello_world.log  /var/log/hello_world.log-20180910
# head -n3 /var/log/hello_world.log
Sep 10 01:34:57 example_host hello_world: 29888
Sep 10 01:34:57 example_host hello_world: Starting Hello World...
Sep 10 01:35:07 example_host hello_world: Hello world!
Sep 10 01:35:17 example_host hello_world: Hello world!
Sep 10 01:35:27 example_host hello_world: Hello world!
# tail -n3 /var/log/hello_world.log
Sep 10 01:34:32 example_host hello_world: Hello world!
Sep 10 01:34:42 example_host hello_world: Hello world!
Sep 10 01:34:52 example_host hello_world: Hello world!
みたいな感じで新しいログファイルに書き始めている事がわかります。
logrotateについては/etc/cron.daily/logrotateにcronファイルがあって、一日一回実行される様になっています。
logrotateには最近hourlyの設定もありますが、cronが一日一回なので
そのままhourly設定をしても1日一回だけになります。
1時間に一回実行したいならこの/etc/cron.daily/logrotateを/etc/cron.hourlyに移すか
別途
/etc/cron.hourlyに
| 1
 |  | 
みたいなファイルを置いたりする必要があります。
まとめ
Systemdの導入によりデーモンの登録が非常に楽になりました。
今回は書いてませんが デーモン同士の依存関係なんかも簡単に書けて、 起動順序とかをサービスファイルに書くだけで簡単に設定できます。
これがかなり協力で、いままで複数のデーモン(サービス)を組み合わせて使いたいときにはユーザーがある程度管理する必要がありましたが Systemdでは設定がきちんと書いてあれば自動的に必要なものを起動したりもしてくれます。
Systemdでも以前のinitファイルもそのままラッパーをかけて使える様な形になってるので 古いサービスも動かすことも出来ます。
ただこれから自作するとするとあのinitスクリプトを書くのはとても面倒で Systemd用のものだけで済ませたいと思うところです。
