cron哀歌～typoを笑うものはtypoに泣く～他

この記事は「本番環境でやらかしちゃった人 Advent Calendar 2019」の12日目です。
https://qiita.com/advent-calendar/2019/yarakashi-production
（想像以上に人気のカレンダーに参加してしまい、正直なところ、戦々恐々としております……）

# はじめに

ほとんどの方ははじめまして、 @NACK と申します。

エンジニアになって何十年も経ちますが、未だに、ここに書いた「やらかし」は夢に見ます。
皆さんにご笑覧いただいて、私も一緒に笑えるようになればいいなあ……と思い、今回の企画に参加させていただきました。
というわけで、ぜひ笑いとばしていってください。もしくは、今後のみなさんの業務に、ほんの少しでもお役に立てれば幸いです。

# 用語説明

+ typoとは 

    入力ミスのこと。"typographical error"の略。
http://e-words.jp/w/%E3%82%BF%E3%82%A4%E3%83%9D.html


# 背景（前提条件）
+ いまの会社の話ではありません。
+ かなり昔の話です（10年以上前）。「古いことやってるなあ」と思われたとしても、実際に古い話なのでスルーしていただけると。
+ cron上でかなりの数のバッチが動いているシステムでした。
+ アプリについてはバージョン管理ツールが導入されていましたが、cronはバージョン管理ツールの対象外で、履歴管理は同一ファイル上で行われていました。

```console:当時の運用ルール
# 下記コマンドでcron設定の編集画面を起動
$ crontab -e

（cron編集前）
10 0 * * * sh hoge.sh 

（cron編集後）
# バッチの実行時刻を0:10→0:30に変更(2019/12/12 by NACK)
# 10 0 * * * sh hoge.sh 
30 0 * * * sh hoge.sh
```

# やらかしは突然に
サブタイトル：typoを笑うものはtypoに泣く

## 問題編

ある日のこと。
私は、本番環境でCRONを更新する準備をしていました。
そこに同僚がやって来ます。

<b>同僚</b>「CRON更新するんだって？やり方教えて！」
<b>私　</b>「いいよ、実際に操作しながら説明するね。 `crontab` ってコマンドを使うのだけれども」
<b>同僚</b>「うん」
<b>私　</b>「 `-l` をつけると中身が見られるから、今の設定が見たいだけならこれを使ってね」
<b>同僚</b>「`hoge.sh`の設定状況を見たい時は？」
<b>私　</b>「コマンドにパイプ付けてgrepすれば良いよ。 `crontab -l | grep hoge.sh` みたいな感じで」
<b>同僚</b>「なるほど」
<b>私　</b>「で、編集する時は `-e` をつけて、と……」
<b>同僚</b>「あっ」
<b>私　</b>「あれ？編集画面がでないな……」
<b>同僚</b>「いま、`e`じゃなくて、隣の `r` 押してたよ」

（注：お気づきの方もいらっしゃるかと思いますが、私、この時点で既にやらかしております。
　　　が、この時点では、２人ともやらかしに気付いておりません）

<b>私　</b>「あ、本当だ。もう一回やるね」
<b>私　</b>「あれ、今度はちゃんと編集画面になったけど、中身がない……？
　　　もしかして、今日って他の部署のメンテナンス日か何かだっけ？？」

　　（メール等の確認作業開始。同僚には周辺部署への確認を依頼）

<b>私　</b>「うーん、やっぱりメールもメッセンジャーも来てないなあ……。
　　　あ、そういえば、さっきコマンド実行時にtypoしたけど、あれって何だったんだろう？」

```console
$ man crontab
（～略～）
-r      Remove the current crontab.
```

<b>私　</b>「は？え？？……えええええええ！！！」
<b>同僚</b>「どうした？」
<b>私　</b>「ど、ど、どうしよう、<b>さっきのtypoでCRON設定全部ふっとばした……！</b>」
<b>同僚</b>「！！……とりあえず落ち着け！」

## その時の思考と行動
社会人になってから最大のやらかしでしたので、文字どおり頭が真っ白になりました。
一緒に作業していた同僚が宥めてくれまして、すぐ上司に報告に行きました。
１人だったらどうなっていたことやら……。

## 原因
- キーボードで、`r`と`e`が隣にあったから
- 削除時に確認を求められなったから
- 本番環境でコマンドを手入力したから

## 対応

実は、「問題編」の中にこんな記述があります。

> <b>私　</b>「 `-l` をつけると中身が見られるから、今の設定が見たいだけならこれを使ってね」

というわけで、私のコンソール上に、消してしまう前のcron設定が……！
（私自身は気が動転していて気が付かなかったのですが、同僚が「さっき中身見たよね！」と教えてくれました）

この「コンソール上のcron設定」を、上司の許可を取った上でコピペ＆整形して復旧しました。

その後、cron設定が復旧するまでの間に動くべきだったバッチの調査、停止したことによる影響調査等で大変なことになったのですが……それはまた、べつのおはなし。

## 振り返り
### 惨劇はなぜおこってしまったのか
- 本番でコマンドを手入力していた（から、typoが発生した）
- 実質一人作業だったため、typoや作業ミスが発生しやすい（同僚と一緒に作業していたものの、確認係ではない＆crontabに詳しいわけでないためミスが発見できない）
- 通常の手順で、typoしただけで惨劇が起きうる状況になっていた

### 二度と惨劇を起こさないためにどうしたのか

#### 本番環境で、コマンドを手入力しない
（crontabは特にひどい、とは言え）通常コマンドでもtypoで想定外の動きをすることはあります。
そのため、本番実行するコマンドはテスト環境でリハーサル＆エディタに保存しておき、本番ではそれをコピペして実行するようにしました。
（時と場合によってはSHELL化したりもしましたが……その辺りはケースバイケースで）

#### 本番作業時はダブルチェックする
基本中の基本だと思います。
（アプリリリースや手動でのバッチ実行については、既にダブルチェック必須だったのですが、cron変更に関しては、そこまでルールが厳しくなく……）

#### 運用ルール変更

下記のように運用ルールが変更されました。

+ cronファイルの履歴管理をするようになりました（この時点では、バージョン管理ソフトによる管理ではなく、同一サーバー上での手動バックアップ）
+ `crontab -e` による直接変更が禁止になり、cronファイルを直接変更する運用になりました。

```console:変更後ルール（１回目）
# とりあえずrootユーザーになる
$ su root
# 現在のCRON設定のバックアップを取る
# cp -p /var/spool/cron/hogehoge /home/hogehoge/cron/hogehoge.20191212
# 編集用ファイル修正
# vi /var/spool/cron/hogehoge
```

（なお、当時は諸事情により対応しませんでしたが「crontabの`r`オプションを封印する」方法もあります。詳しくは [危険な crontab -r を封印する](https://qiita.com/kawaz/items/1620300551b5b3f2eccc) をご覧ください）

当時の私は「これでもう大丈夫だ」とほっとしたものです。
<b>これが、次なるやらかしの原因になるとも知らずに……。</b>


# やらかし　ふたたび

サブタイトル： Timestamp is money.

## 問題編
変更後の運用ルールも軌道に乗り、typoによる心の傷も癒えてきたある日のこと。

とあるバッチの修正リリースの際、「そのバッチの定期実行を停止し、リリース作業完了後に再開させる」という作業が必要になりました。

そのため、ルールどおりに

```console
$ su root
# バックアップ
# cp -p /var/spool/cron/hogehoge /home/hogehoge/cron/hogehoge.20191212
# 目的のバッチ実行をコメントアウトして一時停止
# vi /var/spool/cron/hogehoge
```

し、リリース作業＆手動での実行確認を実施しました。

問題なく確認まで完了し、最後に、定期実行を再開させるため、

```console
# 一時停止版をバックアップ
# cp -p /var/spool/cron/hogehoge /home/hogehoge/cron/hogehoge.20191212.2
# 作業開始前のバックアップをcronファイルに上書き
# cp -p /home/hogehoge/cron/hogehoge.20191212 /var/spool/cron/hogehoge
```

して、作業完了です。


……それから数時間後。

他の部署から「○○バッチから送られてくるはずのデータが届かない。今日のリリースの不具合では？」という問い合わせが来ました。

調査の結果、バッチ自体の改修による問題ではなく、<b>バッチの定期実行が再開されていない</b>ことがわかったのです……。

## 原因
cron設定を戻した時に（`cp`コマンドに`p`オプションをつけたためにタイムスタンプが更新されず）戻したcronファイルのタイムスタンプが、元のファイルより古くなったため。

cronの再起動を行わない場合、cronファイルのタイムスタンプが元のファイルより新しくなっていないと再読み込みをしてくれない＝内容が反映されない、という問題が発生しておりました（この辺りは、OSによって挙動が異なるようです。そもそも、cron自体を再起動しないとダメなものもあるそうで）。

参考：[crontabファイル変更後の反映](http://blog.livedoor.jp/u_26/archives/1819139.html)
※こちらの記事では「変更時刻を毎分チェックして、変更があった場合には再読み込みを行う」と書かれていますが、私が作業していた環境では「変更があった場合」ではなく「元のものより新しい場合」という挙動になっておりました。

## 対応

```console
# touch /var/spool/cron/hogehoge
```

でcronファイルのタイムスタンプを更新し、当該バッチの定期実行が再開したことを確認しました。

その後、そのバッチが動かなかったことによる影響調査＆リストア作業で大変なことになるのですが……これもまた、べつのおはなし。


## 振り返り
### 惨劇はなぜおこってしまったのか
- cronファイルを直接編集した場合「タイムスタンプが古いと反映されない」ということを知らなかったため
- crontabコマンドを使わず、（一般的には）イレギュラーな作業の進め方をしてしまったため

### 二度と惨劇を起こさないためにどうしたのか

再度運用ルールを変更し、crontabコマンドを使うようにしました。
ただし、`e`オプションによる直接変更には懲りていましたし「cronの履歴をバージョン管理ツールで管理したい」という希望も上がっていたので、`e`オプションには頼らない方法を採用しました。

+ cronファイルをバージョン管理対象にし、通常のアプリと同じリリース手順で本番に反映する。

    本番で実行するcron関連コマンドは `crontab [cronファイル]` だけ。

```console:ルール変更２回目
# CRON設定用ファイル（アプリDIR配下。バージョン管理対象）を編集する
$ vi /hoge/fuga/cron/crontab.txt

# コミットする（手順は割愛）

# ～略～

# アプリリリース（手順は割愛）

# リリースされたcron設定用ファイルをcronに反映する
$ crontab /hoge/fuga/cron/crontab.txt
```

このルール変更による副次的効果として

- cron設定がバージョン管理できるようになった
- 当該サーバー以外にもcron設定のバックアップができた
- cron設定の変更自体もプルリク対象になった
- テスト環境にも「同じcron設定」をリリースし、「cron経由」での稼働確認をとる、というフローに変わった→cron経由の時だけ発生する問題（環境変数の問題等）も検知しやすくなった

ということがあったので良かったです。

このルール変更後、cronに関するトラブルはほとんど起きなくなったと記憶しています。
今度こそ、世界は平和になったのです……。

# 終わりに

このアドベントカレンダー2日めの記事
[crontab database ～君がしでかしてくれたもの～](https://qiita.com/raki/items/8aab477b4d64c7e7610e)
の @raki さん作詞の歌に

> 忘れないでね crontab -r

というフレーズがあるのですが、私にとっては「忘れたい、でも忘れられないコマンド」です。
こう書くと、`crontab -r` に失恋したみたいですね（笑）。

# おまけ１
毎年、自社のアドベントカレンダーのほうでも、過去の失敗談（罠に嵌まった話）を書いています（開発段階で気づいた「やらかし未遂」も含まれます）。
こちらのネタと違い、夢に見るほどのやらかしではありませんが……ご用とお急ぎでなければ、是非そちらも楽しんで行ってください！

- [SQLトラップ（ゲームブック風）](https://qiita.com/NACK/items/5ce83d3e76c0fb9b148d)
    （[All About Group（株式会社オールアバウト） Advent Calendar 2019](https://qiita.com/advent-calendar/2019/allabout)）

- [Laravelトラップ（ゲームブック風）](https://qiita.com/NACK/items/65756819b5610d33c849)
    （[All About Group（株式会社オールアバウト） Advent Calendar 2018](https://qiita.com/advent-calendar/2018/allabout)）

# おまけ２：本記事自体のやらかしについて（2019年12月24日追記）
コメント欄でご指摘いただいた内容について、深追いしてみた内容を簡単に書いてみます。
「cron関係ないのもあるよね」というご指摘もあるかと思いますが、クリスマスイブということでお許しください :santa:

## cron設定、今（2019年）ならどうする？
すみません！選択肢が多すぎて、付け焼刃の知識では書けない……と思いました。
が、情報収集した範囲内でまとめてみます。あくまで収集した範囲で……ということでお許しください。

### クラウドサービスを利用している場合
各サービスが便利なスケジューラーを提供しているので、そちらの利用を検討するのが良さそうです。

- Google Cloud
    [バッチ処理のスケジューリングパターン](https://medium.com/google-cloud-jp/batch-scheduling-gcp-e93966110428) （special thanks: @h_amakasu ）

- AWS
    [AWS Batchを使ってcronのような定期的処理を実現する](https://qiita.com/tsukasa1301/items/8bd927eaa928ff981414)

### フレームワークがスケジューラ機能を提供している場合
ケースバイケースではありますが、フレームワークのスケジューラ機能にお任せすると、二重起動防止や「〇〇が終わったら××をする」といった制御もやりやすいです。
怖いcrontabコマンドも、スケジューラの使用準備時に１回だけ実行すれば良い、程度になります。

- Laravel(PHP)
    [Laravel タスクスケジュールまとめ](https://qiita.com/Takuma_Ikeda/items/1323e5a6bea51860ed57)

- Ruby on Rails(Ruby)
    [Railsで定期的にバッチ回す「Whenever」](https://qiita.com/yumiyon/items/388fbb84450f49a6ab0d)

### CRONじゃなきゃダメな人（rootあり）の場合
どうしても「CRON」との付き合いを続けなければならない場合は、`/etc/cron.d` 配下に、個別ファイルを置くのがおススメです（special thanks: @tmiki、@bsd-hacker）。

### CRONじゃなきゃダメな人（rootなし）の場合
どうしても「CRON」との付き合いを続けなければならない、かつrootアカウントも使えない場合は、記事本文の「やらかし　ふたたび」のほうの「[二度と惨劇を起こさないためにどうしたのか](https://qiita.com/NACK/items/c0c0feda4e7a8030346f#%E4%BA%8C%E5%BA%A6%E3%81%A8%E6%83%A8%E5%8A%87%E3%82%92%E8%B5%B7%E3%81%93%E3%81%95%E3%81%AA%E3%81%84%E3%81%9F%E3%82%81%E3%81%AB%E3%81%A9%E3%81%86%E3%81%97%E3%81%9F%E3%81%AE%E3%81%8B-1)」に書かせていただいた方法がおススメです。

もっと手軽に導入するなら、以下の方法が良いかと思います（special thanks: @kitamin）。

```
$ crontab -l > crontab.txt
$ vi crontab.txt
...
$ crontab crontab.txt
```

## アドベントカレンダーを書く時に気をつけたいこと
基本的には、[公式のヘルプページ](https://help.qiita.com/ja/categories/qiita/adventcalendar/)に書かれているとおりです。

- 予約投稿時は、投稿時点ではメンションをつけない。
[予約投稿のために「限定共有投稿」](https://help.qiita.com/ja/articles/qiita-adcal-join#%E4%BA%88%E7%B4%84%E6%8A%95%E7%A8%BF%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)した場合、メンションをつけてしまうとカレンダー公開前にメンションが飛んでしまいます。
参考：[Qiitaの限定共有投稿で意図せずユーザーメンションすると通知がされて迷惑になるので注意しましょう](https://qiita.com/kai_kou/items/1bdd5d80a9e2180eca45)
メンションをつけたい場合は、公開後に編集してメンションをつけましょう。

- 全日埋まっていないと、アドベントカレンダーのランキングに入れない
参考：[Advent Calendar ランキング#参加条件](https://help.qiita.com/ja/articles/qiita-adcal-ranking#%E5%8F%82%E5%8A%A0%E6%9D%A1%E4%BB%B6)
こちら、本件の継続調査をしていて初めて知りました。
最初から埋まっていない場合は諦めもつきますが、後から空いた時は気を付けたいですね。


12月24日の追記分は以上です。
それでは、良いクリスマスイブをお過ごしください……。