cronでsudoを指定したときの多重起動防止について考えてみた

はじめに

こんにちは、omkです。
先日、AIDEの改ざん検知に時間がかかるようでcronで設定した期間内にプロセスが終わらず、多重起動してエラーが出ておりましたのでシェルスクリプトに多重起動を防止する処理を挟むことになりました。
しかし十分にシェルの動きを理解していなかったために多重起動防止処理に毎度引っかかって望むように動かない問題が発生しました。これについて改めて勉強して対策を考えます。

発生した問題

想定としては
①1度目にcronが実行される
②2度目のcronが実行される
③1度目の処理が終わっていない状態で2度目の処理が起動していたら2度目の処理を止める
というイメージでしたが1度目から止まりました。

前提

  • お客様の環境なのでむやみにコマンドを追加できません。
  • ルートユーザーは使えないので、sudo権限のある一般ユーザーのcrontabからシェルスクリプトを実行しています。
  • 中の処理でroot権限が必要なのでsudoをつけてシェルスクリプトを実行します。

このときのcronが以下のようなものです。

* * * * * /usr/bin/sudo /home/centos/aide.sh >/dev/null 2>&1

sudoで/home/centos/aide.shを実行します。内容は改ざん検知を行うもので、多くのファイルを読み取るために時間を要します。

問題に対する解答

非常に初歩的なミスでしたが、

①cronのプロセス
(/bin/sh -c /usr/bin/sudo /home/centos/aide.sh > /dev/null 2>&1)

②sudoでシェルスクリプトを開くプロセス
(/usr/bin/sudo /home/centos/aide.sh)

③シェルスクリプトを実行するプロセス
(/bin/bash /home/centos/aide.sh)

の3つのプロセスが発生することを認識していませんでした。

試したこと(失敗例)

psコマンドとgrepの組み合わせでプロセスを割り出してwc -lでカウント

ProcessCount=$(ps aux | grep /usr/bin/sudo | grep /home/jast/script/aide_check.sh | wc -l)

if [ $ProcessCount -ge 2 ];then
# Processing
exit 1
fi

こちらに関しては/usr/bin/sudo /home/centos/aide.shを割り出し、2個以上だと多重実行だと判定するものでしたが、①cronのプロセス②sudoのプロセスの両方がこれに該当するため失敗していました。
これは3以上にすることで正しく機能しました。
grepを2回に分けているのはgrep自身を結果に含まないようにするためです。

実行中の同名プロセスの中で最も古いもののPIDを現在のプロセスのPID、PPIDと比較

上手くいかなかったので先輩に泣きついて教えてもらったのがこちらのリンクです。
http://pandazx.hatenablog.com/entry/2018/03/01/173521

OLDEST=$(pgrep -fo $0)
if [ $$ != $OLDEST ] && [ $PPID != $OLDEST ]; then
    echo "[ERROR] 二重起動を検知したため、$0 の実行を中止します。"
    exit
fi

こちらは、現在起動している同名のプロセスの中で最も古いものを参照し、現在のプロセスもしくはその親プロセスと一致しない場合を多重起動とみなします。
この場合においても①cronのプロセス、②sudoでシェルスクリプトを開くプロセス、③シェルスクリプトを実行するプロセスがそれぞれ別のプロセスなので多重実行だと判定されて処理が止まります(①のPIDを②、③のPIDと比較する)。

結論

起動している中で最も古いプロセスのPIDを自分のPID、PPIDと比較するのではなく、PGIDを比較します。
PGIDはプロセスグループIDです。
③は②から発生したプロセスで、②は①から発生したプロセスです。つまり、③は①の孫プロセスにあたります。
これらは同じプロセスグループに属し、同じPGIDを有しています。
よって、PGIDを比較することによって同じPGIDであれば一連のプロセス、異なるPGIDであれば多重実行だと判断できます。

先ほどのスクリプトを修正します。

OLDEST=$(pgrep -fo $0)

PGID=$(echo $(ps -p $$ -o pgid | sed 's/[^0-9]//g'))
OLDPGID=$(echo $(ps -p $OLDEST -o pgid | sed 's/[^0-9]//g'))

if [ $PGID -ne $OLDPGID ] ; then
    exit 1
fi

ps -p $$ -o pgidPGID NNNNのように表示されるのでsedコマンドで数字だけ取得します。
これらを変数に格納して比較します。
これで多重実行を防止しつつ、誤動作を防ぐことが出来ました。

(おまけ)他の選択肢

ロックファイルを作る

まずロックファイルを作るやり方ですが、こちらは問題が生じた際にお客様に対処いただくのが困難なので今回は見送ることとしました。以下参考です。
https://qiita.com/hidetzu/items/11f92f941efbb182f757

pidofコマンドで実行前にプロセスを調べる

次にcron実行時にpidofでプロセスを確認する方法ですが、こちらはお客様の環境にコマンドが入っていないため今回は不可能でした。以下参考です。
http://www.meguroman.com/cron_multiple_run_protection/

(おまけ)検証

検証1

試したことで2個目に挙げた処理を用いてデバッグを行いました。

OLDEST=$(pgrep -fo $0)
if [ $$ != $OLDEST ] && [ $PPID != $OLDEST ]; then
    echo PNAME:$0 OLDEST:$OLDEST PPID:$PPID PID:$$ > /home/centos/aidelog
    echo +++++++++++++++++++++++++++++++++ >> /home/centos/aidelog
    pstree >> /home/centos/aidelog
    echo +++++++++++++++++++++++++++++++++ >> /home/centos/aidelog
    ps -p $OLDEST o user,pid,ppid,command >> /home/centos/aidelog
    echo ----------------------- >> /home/centos/aidelog
    ps -p $PPID o user,pid,ppid,command >> /home/centos/aidelog
    echo ----------------------- >> /home/centos/aidelog
    ps -p $$ o user,pid,ppid,command >> /home/centos/aidelog
    exit
fi

先ほどのスクリプトを変更して、プロセスツリーと条件で使用するプロセスの詳細を表示するようにしています。

こちらをcronで実行した結果がこちらです。

PNAME:/home/centos/aide.sh OLDEST:18701 PPID:18702 PID:18703
+++++++++++++++++++++++++++++++++
systemd-+-2*[agetty]
        |-auditd---{auditd}
        |-chronyd
        |-crond---crond---sh---sudo---aide.sh---pstree
        |-dbus-daemon
        |-dhclient
        |-gssproxy---5*[{gssproxy}]
        |-httpd---6*[httpd]
        |-mysqld---26*[{mysqld}]
        |-polkitd---6*[{polkitd}]
        |-rpcbind
        |-rsyslogd---2*[{rsyslogd}]
        |-2*[sendmail]
        |-sshd---sshd---sshd---bash
        |-systemd-journal
        |-systemd-logind
        |-systemd-udevd
        \-tuned---4*[{tuned}]
+++++++++++++++++++++++++++++++++
USER       PID  PPID COMMAND
centos   18701 18700 /bin/sh -c /usr/bin/sudo /home/centos/aide.sh > /dev/null 2>&1
-----------------------
USER       PID  PPID COMMAND
root     18702 18701 /usr/bin/sudo /home/centos/aide.sh
-----------------------
USER       PID  PPID COMMAND
root     18703 18702 /bin/bash /home/centos/aide.sh

psの結果は上からcronのプロセス、sudoのプロセス、スクリプト実行のプロセスであることがわかります。
こちらに関してはpstreeの結果から現在のプロセスの親プロセスだけでなく、そのさらに親のプロセスまで見る必要があったために失敗していたことがわかります。

検証2

PGIDの取得が上手く行くか検証を行いました。
cronで以下のスクリプトを毎分実行します。

#!/bin/bash

echo "**********************" >> pgid.txt
date >> pgid.txt
OLDEST=$(pgrep -fo $0)
PGID=$(echo $(ps -p $$ -o pgid | sed 's/[^0-9]//g'))
OLDPGID=$(echo $(ps -p $OLDEST -o pgid | sed 's/[^0-9]//g'))
echo $PGID >> pgid.txt
echo $OLDPGID >> pgid.txt
if [ $PGID -ne $OLDPGID ] ; then
    echo "BAD" >> pgid.txt
    exit 1
fi

echo "GOOD" >> pgid.txt
sleep 90
exit 0

n回目のプロセスは多重に実行されていなければ「GOOD」と表示され、90秒待ちます。
n+1回目のプロセスはn回目が90秒待っている間に起動するので多重実行に引っかかり「BAD」と表示されます。

-crond-+-crond---sh---sudo---pgid
       \-crond---sh---sudo---pgid

実行時のプロセスツリーが上のものです。
プロセスが分岐しているのでPGIDが異なります。
「pgid.txt」をtail -fで確認して、GOODとBADが交互に出ていましたので多重実行の防止として機能していることを確認しました。

まとめ

結構いろんな場面で使えるものが出来たように思います。

返信を残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA