Rundeck の Kill Job や Timeout でプロセスが生き続ける問題の対処方法

Rundeck の “Kill Job” や timeout がちゃんと動いてないという issue は何個も上がっていて、rundeck/#2911 辺りにまとめられているんですが、ここでは server node で (Execute locally) inline script や script file を実行する場合を扱います。

inline script の実行の仕組み

どうも次の手順で inline script を実行しているみたいです。local script/remote script もスクリプトの内容を取得するロジックが違うだけで、同じパスを通るみたいです。

  1. inline script を tempfile に書き出す
  2. 作成したファイルに実行権限を付与する
  3. 作成したファイルを実行する
  4. 作成したファイルを削除する

何故 “Kill Job” や Timeout が機能しないのか

inline script を実行する上で肝になるのが LocalNodeExecutor#executeCommand です。このメソッド内で ExecTaskParameterGeneratorImpl#generate を実行するわけですが、必ず scriptfile 引数が null になり、command として実行権限を付与したファイルのパスが渡されます。scriptfilenullcommand が指定されている場合は /bin/sh -c 経由で作成したスクリプトを実行することになります。

そして、Rundeck が “Kill Job” で SIGTERM を送るのは /bin/sh -c のプロセスだけです。
よって、”Kill Job” が機能しないのは /bin/sh -c が起動した子プロセスにシグナルが送られないことが原因です。

対処方法

次のような inline script を考えます。

sleep 60

この書き方だと “Kill Job” をしてもプロセスが生き続けます。これは実際に /bin/sh -c のプロセスに SIGTERM を送ってみるとよくわかります。

% docker run --rm -it centos:centos7 bash
[root@e0d5d4380043 /]# echo sleep 60 > tmp.sh
[root@e0d5d4380043 /]# chmod +x tmp.sh
[root@e0d5d4380043 /]# /bin/sh -c ./tmp.sh &
[1] 14
[root@e0d5d4380043 /]# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.2  0.1  11788  2908 pts/0    Ss   20:37   0:00 bash
root        14  0.0  0.1  11648  2584 pts/0    S    20:37   0:00 /bin/sh -c ./tmp.sh
root        15  0.0  0.0   4328   672 pts/0    S    20:37   0:00  \_ sleep 60
root        16  0.0  0.1  47456  3436 pts/0    R+   20:37   0:00 ps auxf
[root@e0d5d4380043 /]# kill 14
[root@e0d5d4380043 /]# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.1  0.1  11788  2908 pts/0    Ss   20:37   0:00 bash
root        15  0.0  0.0   4328   672 pts/0    S    20:37   0:00 sleep 60
root        17  0.0  0.1  47456  3336 pts/0    R+   20:37   0:00 ps auxf
[1]+  Terminated              /bin/sh -c ./tmp.sh

/bin/sh -c ./tmp.sh が終了しても sleep 60 のプロセスが残っていることがわかると思います。
そこで次のように exec を使うようにします。

exec sleep 60

試してみます。

[root@e0d5d4380043 /]# echo exec sleep 60 > tmp_with_exec.sh
[root@e0d5d4380043 /]# chmod +x tmp_with_exec.sh
[root@e0d5d4380043 /]# /bin/sh -c ./tmp_with_exec.sh &
[1] 20
[root@e0d5d4380043 /]# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1  11788  2916 pts/0    Ss   20:37   0:00 bash
root        20  0.0  0.0   4328   636 pts/0    S    20:39   0:00 sleep 60
root        21  0.0  0.1  47456  3376 pts/0    R+   20:39   0:00 ps auxf
[root@e0d5d4380043 /]# kill 20
[1]+  Terminated              /bin/sh -c ./tmp_with_exec.sh
[root@e0d5d4380043 /]# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1  11788  2916 pts/0    Ss   20:37   0:00 bash
root        22  0.0  0.1  47456  3340 pts/0    R+   20:40   0:00 ps auxf

ちゃんと sleep 60 も終了しましたね!

exechelp exec にあるように、shell を指定したコマンドで差し替えるコマンドです。

exec: exec [-cl] [-a name] [command [arguments ...]] [redirection ...]
    Replace the shell with the given command.

    Execute COMMAND, replacing this shell with the specified program.
    ARGUMENTS become the arguments to COMMAND.  If COMMAND is not specified,
    any redirections take effect in the current shell.

    Options:
      -a name   pass NAME as the zeroth argument to COMMAND
      -c                execute COMMAND with an empty environment
      -l                place a dash in the zeroth argument to COMMAND

    If the command cannot be executed, a non-interactive shell exits, unless
    the shell option `execfail' is set.

    Exit Status:
    Returns success unless COMMAND is not found or a redirection error occurs.

これによって Rundeck が /bin/sh のプロセスに SIGTERM を送ったにも関わらず exec 経由で実行したコマンドのプロセスに SIGTERM が送られることになります。

それでもダメなケース

CentOS の /bin/shbash のシンボリックリンクなんですが、Debian の /bin/shdash のシンボリックリンクです。これによって子プロセスの作られ方が変わってきます。

先ほどは CentOS の Docker image を使いましたが、今度は Debian の Docker image を使ってみます。

% docker run --rm -it debian:jessie bash
root@3f63206e332e:/# echo sleep 60 > tmp.sh
root@3f63206e332e:/# chmod +x tmp.sh
root@3f63206e332e:/# /bin/sh -c ./tmp.sh &
[1] 6
root@3f63206e332e:/# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.3  0.1  20248  3180 pts/0    Ss   20:47   0:00 bash
root         6  0.0  0.0   4340   684 pts/0    S    20:47   0:00 /bin/sh -c ./tmp.sh
root         7  0.0  0.0   4340   752 pts/0    S    20:47   0:00  \_ /bin/sh ./tmp.sh
root         8  0.0  0.0   4240   692 pts/0    S    20:47   0:00      \_ sleep 60
root         9  0.0  0.1  17500  2076 pts/0    R+   20:47   0:00 ps auxf

CentOS の場合は /bin/sh -c ./tmp.shsleep 60 を起動していましたが、Debian の場合は /bin/sh -c ./tmp.sh/bin/sh ./tmp.sh を起動し、/bin/sh ./tmp.shsleep 60 を起動していますね!

よって、Debian の場合は exec sleep 60 にしても sleep 60 が生きたままになります。

root@3f63206e332e:/# echo exec sleep 60 > tmp_with_exec.sh
root@3f63206e332e:/# chmod +x tmp_with_exec.sh
root@3f63206e332e:/# /bin/sh -c ./tmp_with_exec.sh &
[1] 11
root@3f63206e332e:/# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1  20248  3180 pts/0    Ss   20:47   0:00 bash
root        11  0.0  0.0   4340   768 pts/0    S    20:51   0:00 /bin/sh -c ./tmp_with_ex
root        12  0.0  0.0   4240   668 pts/0    S    20:51   0:00  \_ sleep 60
root        13  0.0  0.1  17500  2088 pts/0    R+   20:51   0:00 ps auxf
root@3f63206e332e:/# kill 11
root@3f63206e332e:/# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1  20248  3180 pts/0    Ss   20:47   0:00 bash
root        12  0.0  0.0   4240   668 pts/0    S    20:51   0:00 sleep 60
root        14  0.0  0.1  17500  2084 pts/0    R+   20:51   0:00 ps auxf
[1]+  Terminated              /bin/sh -c ./tmp_with_exec.sh

これに対処するには “Invocation String” に exec を指定します。Step を追加する際に “Script” や “Script file or URL” を指定すると “Advanced” の中で指定することができます。

YAML で Job の内容を書くと次のように scriptInterpreterexec を指定することになります。

- description: ''
  executionEnabled: true
  id: a81da3db-f450-4825-a615-f44caf2baf31
  loglevel: INFO
  name: sleep
  nodeFilterEditable: false
  scheduleEnabled: true
  sequence:
    commands:
    - interpreterArgsQuoted: false
      script: exec sleep 600
      scriptInterpreter: exec
    keepgoing: false
    strategy: node-first
  uuid: a81da3db-f450-4825-a615-f44caf2baf31

それでも困ること

“Log Output” に “Kill Job” や timeout 後のログが流れないの何とかならないですかね…。Graceful stop したいケースもあるだろうに。

おまけ

IntelliJ IDEA を使った Rundeck のデバッグ方法についてです。普段全然 IntelliJ を使わないし Java も全然読み書きしないので非効率なやり方かもしれないですが、とりあえず以下の手順でデバッグできます。

まず rundeck リポジトリ直下で次のコマンドを実行することでプロジェクトを作成します。

./gradlew idea

次に Import Project で rundeck リポジトリを選択して取り込みます。

あとは Edit Configurations… で Remote を追加して、”Command line arguments for running remote JVM” に書いてある引数を jar ファイルを実行する際に指定し、Debug を実行して接続すればブレークポイントを埋め込んだり色々できます。


./gradlew build -x javadoc -x test
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 \
  -Xmx1024m -Xms256m -XX:MaxMetaspaceSize=256m -server \
  -jar rundeck-launcher/launcher/build/libs/rundeck-launcher-2.10.7-SNAPSHOT.jar