systemctl（systemd）を使ったジョブの定期実行と永続化

　最近サーバーを1つに統合（CentOS→ubuntuに統合）した際にサーバーサイドの設定で定期的に処理するジョブを自動化かつ永続化したかったので、cronからsystemdに乗り換えてみました。実際にやってみてハマりどころも多かったので、参考になればと思い忘備録兼ねて共有させてもらいます。
【重要】もし実際にsystemdを使う場合には、サーバーが不安定になってしまう場合も十分あり得るので、必ずスナップショット等でバックアップを事前に行いリストア可能にしておくことを強くお勧めします。あとsystemdはバージョンが古いからといって安易なバージョンアップは危険なようですね。


 [　リンク：RedHat systemdの概要　](https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/8/html/configuring_basic_system_settings/introduction-to-systemd_configuring-basic-system-settings)


　この記事の主な内容はこれです。
１．systemdはクセが強いが強い味方になる
２．ハマりどころと対処策
３．timer,service,targetの具体的な設定方法の参考例

１．systemdはクセが強いが強い味方になる
　ubuntuでは15.04以降systemdが使われているようですが、今回古いCentOSをubuntu18.04にマイグレした際に、Let's EncryptやNginxの設定をいじる機会があり、これらも含めてサーバー起動時の自動起動（永続化）はsystemdが使われていることを再認識しました。ただし次で詳述しますが、かなりクセが強くルール通り設定しないと暴走したりするので注意が必要です。しかもルールや設定方法のサンプルが残念ながらアテにならないケースも多く（多分OSに近い部分なので環境やVersionによって違うことが多いのかもしれません）、修正回数も半端なく多くなりがちな感想を持ちました。
　そんなsystemdですが、最終的にミス無く設定をしてしまえばあとは安定して定期的な処理を自動でやってくれるし、systemdで永続化しておけばサーバーが再起動した際、アプリに必要なサーバーサイドのNodeプロセス等をキチンと自動起動してくれるのでとても助かります。Centを使っていたと時はforeverで起動しておけばサーバーが再起動した際にも自動起動してくれたのですが、ubuntuに移行した後は起動しなくなっていたので、今回はforever自体をsystemdで永続化しました。とてもsystemdの全貌を説明する力量は無いので、今回はファイルの種別で言うとservice,target,timerの3つについて触れたいと思います（以下サービス等とはこの３種類のファイルを指します）。
　よく使うコマンド＆オプションは次の通りです（通常はこれらのsystemd設定ファイルは/etc/systemd/system/に配置しますので、下記はこのディレクトリ直下にいる前提です）。

```コマンド＆オプションリスト.
すべてのユニットファイルを再読み込みし変更を適用させる
　systemctl daemon-reload
全サービス等の起動状態確認
　systemctl list-units -t service(or timer or target)
サービス等の起動/再起動/停止
　systemctl start xxxx.service(or timer or target)
　systemctl restart xxxx.service(or timer or target)
　systemctl stop xxxx.service(or timer or target)
サービス等の有効化(永続化のこと）/無効化
　systemctl enable xxxx.service(or timer or target)
　systemctl disable xxxx.service(or timer or target)
サービス等の状態確認
　systemctl status xxxx.service(or timer or target)
各サービス等が自動起動するか否かを確認するコマンド
　systemctl list-unit-files -t service(or timer or target)
特定サービス等を狙い撃ちで自動起動するかどうかの確認
　systemctl is-enabled xxxx.service(or timer or target)
特定サービス等を狙い撃ちで起動中かどうかの確認
　systemctl is-active xxxx.service(or timer or target)
systemctlファイルの検証
systemd-analyze verify xxxx.service(or timer or target)

以下は直接systemdと関係はありませんが、よく使うコマンドを参考までに。
ログファイルでのエラーの有無の確認（ログ排出先を指定しない場合はsyslogにログが残ります）
　less /var/log/syslog
コマンド自体のパスの確認（コマンドによってパスが異なります）
　which コマンド名（catやfindやcpなど）
```

２．ハマりどころと対処策
 今回自分の使用した環境は次の通りです。少し古い環境なのでハマりが多かった所もあるかと。
・検証環境：ubuntu :v18.0.4, systemctl:237
　まずはserviceファイルですが、このファイルは永続化したり定期実行したりする処理の内容を記述する設定ファイルで、[Unit],[Service],[Install]の各項目を設定して行きます。targetファイルは、複数のserviceファイルを一括して永続化したり定期実行したりする場合に使いますので、1つのserviceファイルしか扱わない場合や一括処理ではなく個別に扱う場合は不要です。serviceファイルとtargetファイルの関連付けの方法は単純で、serviceファイル側の[Unit]内にPartOf=xxxx.targetでtargetファイルの一部であることを宣言し、targetファイル側の[Unit]内でWants=xxxx.serviceを列挙して複数のserviceファイルを一つのグループにまとめる設定をします（具体例は下記を参照して下さい）。
　一方timerファイルで定期実行をしたい場合は、serviceファイル名と同じファイル名（拡張子の左側部分のみ）のtimerファイルにする事により関連付けされるので、ファイル名が同一であることが必要です（例：hoge.serviceに対してはhoge.timerで定期実行が処理されます）。
　以上は基本の使い方の予備知識程度ですが、この程度では上手く行かない点が多くあるので私が経験したハマりどころをファイルの種類毎に紹介して行きます。
①serviceファイルの設定のハマりどころ
　a. パスの設定方法
　[service]にExecStartで処理したいコマンドラインを記述するのですが、例えばfindコマンドで３０日以上経過した/home/user/傘下にある.txtファイルを全て削除したい場合に、

```ダメな例.
ExecStart=find /home/user/ -name '*.txt' -mtime +30 -delete
```

では動きません。先に動く例を紹介すると、次の通りです。

```OKだった例.
ExecStart=/usr/bin/find /home/user/ -name '*.html' -mtime +30 -delete
```

コマンドにも絶対パスが必要です（whichコマンドで確認して下さい）。


　この事例を経験したので絶対パスで全て書いてしまえば大丈夫と思い（これはとんでも無くハマりましたが）/usr/local/bin/app傘下のapp.jsのnodeプロセスを永続化したい場合に、下記のように書いて失敗しました。

```ダメな例.
ExecStart=/usr/local/lib/node_modules/forever/bin/forever start /usr/local/bin/app/app.js
```

これではapp.js内で相対パスを使ってデレクトリを新規作成する処理が、勝手にルート直下に作成する処理にすり替わってしまい（つまりapp.js内まで絶対パスで処理されるようになる！）とんでもない事になりました。これは次のように[service]内にWorkingDirectoryの設定を追加し、

```OKだった例.
ExecStart=/usr/local/lib/node_modules/forever/bin/forever start app.js
WorkingDirectory=/usr/local/bin/app/
```

ExecStartのapp.jsのパス指定を削除することで正常に動作するようになりました。

　b. パイプ（｜）やリダイレクト（＞等）が効かない
　タイトル通り[service]のExecStartに記述したパイプ（｜）やリダイレクト（＞等）は動作しません。

``` ダメな例.
ExecStart=/bin/cat /home/usr/list.txt | xargs rm -v | : > /home/usr/list.txt
```

list.txt内の削除対象ファイルは絶対パスで記述されているので問題はないはずですが、要はパイプやリダイレクトは使えないようです。なのでスクリプトファイルを作成し、処理内容はそこに書いて、serviceファイルで呼び出すだけにします。下記の構成で動きました。

```xxxx.serviceファイル
ExecStart=/bin/bash /etc/systemd/system/del.sh
```
```del.shファイル
/bin/cat /home/usr/list.txt | xargs rm -v | : > /home/usr/list.txt
```

②targetファイルの設定
　上記通りserviceファイルとの関連付けは比較的単純なので、あまりハマりどころは無かったのですが、プロセス数が変わったとき（xxxx.target の Wants=xxxx.serviceが増減したとき）、systemctl restartだけだと反映されないので、systemctl stop → systemctl daemon-reloadでリロード　→ systemctl startのステップを踏んで下さい。あとは起動したり永続化したりする対象はtargetファイルになり、systemctl start xxxx.target, systemctl enable xxxx.targetととします。これで複数のserviceを一括で起動したり永続化できるので便利ですが、逆に1つのserviceで不具合等がありrestartやreloadする場合に他のserviceも影響を受けるので、一長一短があるのも事実です。単独で扱いたい場合はxxxx.serviceで起動・永続化をして下さい。

③timerファイルの設定
　上記のようにserviceファイルとtimerファイルはファイル名を同一にする必要があり、timerファイルはserviceファイルと対になっているようなので、例えば複数serviceファイルをtargetで束ねた結果のhoge.targetをhoge.timerで定期実行しようとしても動きません。timerの性質上同時に複数のserviceを定期実行すること自体がもしかしたらNGかもしれません。私の場合は結局少し時間をずらして2つのserviceを別々のtimerで実行するように変更して対処しました。

以上が各ファイル別の対処方法ですが、次のコマンドで各ファイルの検証が出来るので、躓いた時には試してみると参考にはなると思います（エラーっぽいメッセージが出る場合は大抵実行も失敗しますが、メッセージの内容はあまり親切では無いので惑わされないようにもして下さい）。

```ファイル検証コマンド.
systemd-analyze verify xxxx.service(or timer or target)
```

３．timer,service,targetの具体的な設定方法の参考例
　①パイプやリダイレクを含むファイル削除処理を定期実行し永続化する例
以下のファイルを全て/etc/systemd/system/直下に配置し、実行コマンドを実行します。最後のsystemctl statusで状態を確認できます。

```del.sh
/bin/cat /home/usr/list.txt | xargs --no-run-if-empty rm -v |  : > /home/usr/list.txt
```
```del.service
[Unit]
Description=Delete files listed in list.txt
After=network.target

[Service]
ExecStart=/bin/bash /etc/systemd/system/del.sh
```

```del.timer
[Unit]
Description=timer for del.service
After=network.target

[Timer]
OnCalendar=*-*-* 3:00:00

[Install]
WantedBy=timers.target
```

```実行コマンド.
/etc/systemd/system/#systemctl daemon-reload
/etc/systemd/system/#systemctl start del.timer
/etc/systemd/system/#systemctl enable del.timer
/etc/systemd/system/#systemctl status del.timer
```
これで毎日深夜3:00にlist.txtに記載されたファイルを削除し、list.txtファイルを空きファイルにリセットします。なおlist.txtが既に空きファイルの場合は--no-run-if-emptyのオプションで削除プロセスが走らないようにしてます。

②複数のNodeプロセスを一括永続化する例
　この例ではforeverを使ってNodeプロセスを起動し、サーバーが再起動した際の永続化をsystemdで行なった例です（本来はforeverの使用で永続化出来てないとおかしいのかもしれません）。

```forever1.service
[Unit]
Description = forever service
After = netwark.target
PartOf=forever.target

[Service]
ExecStart=/usr/local/lib/node_modules/forever/bin/forever start app1.js
ExecStop=/usr/local/lib/node_modules/forever/bin/forever stop app1.js
Restart=always
RestartSec=10
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=node-process
Type=forking
Environment=NODE_ENV=production
WorkingDirectory=/usr/local/bin/app1/

#forever2.service, forever3.serviceも上記app1をapp2,app3に読み替える内容で設定する。

```

```forever.target
[Unit]
Wants=forever1.service
Wants=forever2.service
Wants=forever3.service

[Install]
WantedBy=multi-user.target
```


```実行コマンド.
/etc/systemd/system/#systemctl daemon-reload
/etc/systemd/system/#systemctl start forever.target
/etc/systemd/system/#systemctl enable forever.target
/etc/systemd/system/#systemctl status forever.target
```

これでapp1.js,app2.js,app3.jsの3つのNodeプロセスが一括で起動と永続化ができました。WorkingDirectoryをアプリ毎に別々にキチンと設定すれば、各app*.js内の処理はローカルで検証した通りの動きをしてくれるはずです。