crontab から RUNDECK への移行

# 背景
現在、既存システムの「SPOFを撲滅しよう」という掛け声のもと着々とインフラを移行していますが、盲点だったのがバッチ処理でした。
Webサーバを冗長化しても、cron で設定しているのは1台だけ。その1台をメンテで止めたり、障害で止まったりした場合は、別のサーバで手動で crontab をバックアップからもどして復旧してあげる必要があり、しかもどこまでどのバッチが動いていたのかを知るのが大変でした。

また、冗長性以前の問題として、複数のサーバでいろんなバッチが動いており、そもそも、どのサーバで何の cron が動いているのか、依存関係があるバッチはちゃんと前のバッチが終了してから次のバッチが起動しているのか、といったことが管理できていませんでした。

そこで、バッチをちゃんと管理しよう、ということで OSS のジョブスケジューラーである RUNDECK を導入することにしました。

ここでは、既存の crontab から RUNDECK への移行方法を記録しておきます。

前提：Amazon Linux

# RUNDECK の構築
RUNDECK自体のインストールは yum で一発。非常に簡単です。
このあたりを参考にさせていただきました。
http://dev.classmethod.jp/server-side/server/try-rundeck-job/
https://heartbeats.jp/hbblog/2015/01/rundeck.html

ただ、2016年11月30日にバージョンが 2.7系になり、必要な Java のバージョンも 1.7系から1.8系へと変わりました。

```console
# yum install java-1.8.0 
# rpm -Uvh http://repo.rundeck.org/latest.rpm 
# yum install rundeck 
# service rundeckd start
```
たったこれだけです。
ただ、デフォルトで localhost からのアクセスしかできないようになっているので下記の2ファイルのURLを変更しましょう。

```console
# vim /etc/rundeck/framework.properties
framework.server.url = http://<ホスト名>:4440
# vim /etc/rundeck/rundeck-config.properties
grails.serverURL=http://<ホスト名>:4440
# service rundeckd restart
```
この RUNDECK からロードバランサを通じてバッチサーバ（Webサーバ）にアクセスすればその時に生きているサーバでバッチが実行できます。めでたし！

でも、この RUNDECK 自体のサーバが止まったらどうすんの！という話になりますが、なんと RUNDECK はクラスター機能を標準で持っていて設定も非常に簡単です。
設定ファイルでクラスター機能をONにして、DBの接続先（RDS）とログの格納先（S3）を設定するだけでいけます。素晴らしい！
クラスター化については、[こちら](http://tech.im-dmp.net/entry/archives/5114)を参考にしてください。

# RUNDECK CLI の導入
RUNDECK はGUIでジョブの登録もできるし、実行結果も見られるので非常に便利なのですが、今回、cron から移行しないといけないバッチの数は軽く100を越えます…
GUIでチクチクやってられない！

というわけで、RUNDECK CLI の出番です。
CLI から YAML（またはXML）形式でジョブの登録ができます。

RUNDECK は元々、rd-* という名称の Command Line Tool が付属していましたが、2.7系からはそれが大きく変わり、RD という名称で別パッケージになりました。
https://rundeck.github.io/rundeck-cli/
インストールは本体と同じく yum 一発です。

```console
# wget https://bintray.com/rundeck/rundeck-rpm/rpm -O bintray.repo
# mv bintray.repo /etc/yum.repos.d/
# yum install rundeck-cli
```

これで rd コマンドが使えるようになりますが、環境変数でいろいろ設定しておくといちいちコマンドラインで指定しなくてよいので楽ちんです。

```bash
# vim ~/.bashrc
export RD_USER=<RUNDECKのユーザ名>
export RD_PASSWORD=<パスワード>
export RD_URL=http://<ホスト名>:4440/api/18
export RD_PROJECT=<RUNDECKのプロジェクト名>
```
RD_PROJECT はプロジェクト名が固定でなければ、コマンドラインで -p でも指定できます。

# crontab からの変換
これで準備は整ったので、あとは crontab の中身を YAML にして RUNDECK に飲み込ませるだけですが、

```console
$ rd jobs load --file foo.yml -F yaml
```
YAMLの[フォーマット](http://rundeck.org/docs/man5/job-yaml.html)。結構ややこしい…

というわけで、crontab のファイルを読み込んで RUNDECK 用のYAMLに変換するスクリプトを作りました。
前提条件
* /var/spool/cron/<ユーザ名> のフォーマット形式にのみ対応しています
* /etc/cron.\*/\* のように各行に実行ユーザ名が入る形式はちょっとスクリプトを変更する必要があります
* 曜日指定で起動するバッチはウチにはなかったので対応してません。スケジュールのYAMLを作るところで場合分けをして吐き出すデータを変更する必要があります
* ジョブの名前はコマンドをそのまま入れてます。ただ、名前に「/」が使えなかったので「_」に変換してあります

```bash:c2r.sh
#!/bin/bash
GLOBIGNORE=*
RD='/usr/bin/rd'
YAML='jobs.yml'

# 引数で実行ホスト名を指定
if [ $# -eq 1 ]; then
  host="$1"
else
  echo "Usage: $0 <hostname> < <cron file>"
  exit 1
fi

# 標準入力より読み込み、有効な crontab の形式のみ抽出
reg="^([0-9 \*/,-]+)(.+)$"
while read line ; do
  if [[ $line =~ $reg ]]; then
    schedule="${BASH_REMATCH[1]}"
    cmd="${BASH_REMATCH[2]}"
    name=${cmd//\//_}

    # 実行するコマンドが / 始まりだと schedule の方に食われちゃうので
    # / をコマンドの方に移す
    if [[ "${schedule: -1}" = "/" ]]; then
      schedule="${schedule% /}"
      cmd="/$cmd"
    fi

    # schedule を月日時分に分割
    set -- $schedule
    min="$1"
    hour="$2"
    day="$3"
    month="$4"
    week="$5"

    cat >> $YAML <<EOD
- description: ''
  executionEnabled: true
  name: $name
  nodefilters:
    filter: $host
  schedule:
    dayofmonth:
      day: '$day'
    month: '$month'
    time:
      hour: '$hour'
      minute: '$min'
      seconds: '0'
    year: '*'
  scheduleEnabled: true
  sequence:
    commands:
    - exec: $cmd
    keepgoing: false
    strategy: node-first
EOD

  fi
done

if [ -e $YAML ]; then
  $RD jobs load --file $YAML -F yaml
fi

unset GLOBIGNORE
```
実行は、バッチを実行するホスト名を引数にして、標準入力に crontab ファイルを流し込みます

```console
$ ./c2r.sh <バッチ実行ホスト名> < <crontabファイル>
```

# まとめ
RUNDECK で SPOF の解消と、cron のカオスから解放されましょう。