The Nightmare Monkey before Christmas


本稿はLivesense - 自 Advent Calendar 2017の16日目の記事です。
リブセンスでアルコール駆動インフラエンジニアをやっている @Ets です。

はじめに

背景

2017年末の現在、リブセンスのサービスは障害はもちろん、アラートすらほとんど発生しない環境となりました。
これは、全社のエンジニアの努力の積み重ねの賜物です。心から感謝。

しかし我々インフラグループは、未知のアラートが飛んできたときに慌てずに対処することが求められるグループです。
あまりにもアラートが無いと、いざ本当の障害という時に焦ってしまうかもしれません。

焦ってしまうのは、新人が配属されたときも同様でしょう。
初めてのアラート対応が本番というのも可哀想なもの。

だから、いつの頃からか、こんなことを言うようになりました。

『開発環境で障害対応訓練をできないだろうか』

擬似的な障害を起こす

NetflixのChaos Monkeyはご存知でしょうか。
ITエンジニア、運用系に携わる人なら聞いたことがあるかもしれませんね。
どんなものかは、Publickeyのこの記事にわかりやすくまとまっています。(以下引用)

あえてわざと管理された範囲内の障害を日常的に引き起こし、それに日常的に対応できていることを証明し続けることで、本物の障害が起きても問題なく対処できることが証明できる

しかし、Chaos Monkeyが対象としているのは基本的にインスタンス単位の障害です。
もっと小さな、プロセス単位で障害を起こしたいものです。

無いなら作るしかないですね。

Nightmare Monkey概要

Nightmare MonkeyはChaos Monkeyにインスパイアされた、OSに悪夢を招くプログラムです。

OS上でdaemonとして動作し、ランダムにOSに不具合を発生させていきます。
※ 本稿執筆時点では、ランダムにプロセスをKILLする機能しか実装されていません。

実装

Go言語で書かれています。
実装には特にテクニカルな面はないので、説明は割愛します。詳しく知りたいならGitHubを見てもらいたいところですが、このアドベントカレンダーのネタに思いついて急いで書いたコードなので考慮も実装もテストコードすらも不足しています。

CentOS上でのみ動作テストをしているので、その辺もご容赦ください。

簡単な使い方

簡単も何も・・・そもそも簡単な機能しか実装してないんですけどね。

Dryrun

オプションを特に何も指定せず実行するとDryrunモードで動きます。
まずはこれで様子見してください。

Dryrun実行例1
$ nightmare-monkey

2017/12/14 16:32:02 Nightmare Monkey is playing from 11:00 to 18:00, Mon thru Fri.
2017/12/14 16:32:02 Next: 4365 seconds after

上記例では随分先に事故が起きそうです。
Dryrunでは、もう少し短い間隔(オプションについては後述)を指定したほうが良いでしょう。

Dryrun実行例2
$ nightmare-monkey --min-interval 1 --max-interval 2

2017/12/14 16:32:40 Nightmare Monkey is playing from 11:00 to 18:00, Mon thru Fri.
2017/12/14 16:32:40 Next: 1 seconds after
2017/12/14 16:32:41 (Dryrun) Kill: sshd(PID:23183 PPID:1) - by monkey
2017/12/14 16:32:41 Next: 1 seconds after
2017/12/14 16:32:42 (Dryrun) Kill: rsyslogd(PID:1013 PPID:1) - by monkey
2017/12/14 16:32:42 Next: 1 seconds after
2017/12/14 16:32:43 (Dryrun) Kill: acpid(PID:1156 PPID:1) - by monkey
2017/12/14 16:32:43 Next: 1 seconds after

ランダム時間経過するたびにプロセスをKILLする様子が見れますが、Dryrunなので何も起きません。

通常動作

動きそうな気がしたら、--executeオプションを付けましょう。
「OSを不安定にする」という機能のため、root権限で実行しましょう。
nohupなどと組み合わせてdaemon化しておき、ログは答え合わせ程度に利用するのがお勧めです。

--executeオプションで起動すると不規則にプロセスを殺そうとします。
完全なランダムではなく、以下の条件で動くようにはしています。

  • 指定した最小間隔(min-interval, default:3600=1時間)と最大間隔(max-interval, default:21600=6時間)の間
  • 指定した曜日(day, default:1-5=月〜金)
  • 指定した時間(time, default:11:00-18:00)

つまり、ゆるめに平日業務時間をデフォルトにしています。

なお、以下ではmin-intervalmax-intervalにひどい数字を指定して酷い目にあったログを貼っておきます。

実行ログ
$ sudo nohup nightmare-monkey --execute --min-interval 1 --max-interval 2 > /vagrant/nightmare.log 2>&1 &
[1] 11103

$ tail -n 1000 -f /vagrant/nightmare.log

2017/12/14 16:39:00 Nightmare Monkey is playing from 11:00 to 18:00, Mon thru Fri.
2017/12/14 16:39:00 Next: 1 seconds after
2017/12/14 16:39:01 Kill: kacpid(PID:17 PPID:2) - by monkey
2017/12/14 16:39:01 Next: 1 seconds after
2017/12/14 16:39:02 Kill: usbhid_resumer(PID:41 PPID:2) - by monkey
2017/12/14 16:39:02 Next: 1 seconds after
2017/12/14 16:39:03 Kill: ata_sff/0(PID:21 PPID:2) - by monkey
2017/12/14 16:39:03 Next: 1 seconds after
2017/12/14 16:39:04 Kill: mingetty(PID:23607 PPID:1) - by monkey
2017/12/14 16:39:04 Next: 1 seconds after
2017/12/14 16:39:05 Kill: rpc.idmapd(PID:1107 PPID:1) - by monkey
2017/12/14 16:39:05 Next: 1 seconds after
2017/12/14 16:39:06 Kill: rpciod/0(PID:1103 PPID:2) - by monkey
2017/12/14 16:39:06 Next: 1 seconds after
2017/12/14 16:39:07 Kill: sshd(PID:10423 PPID:23183) - by monkey
2017/12/14 16:39:07 Next: 1 seconds after
2017/12/14 16:39:08 Kill: ksuspend_usbd(PID:22 PPID:2) - by monkey
2017/12/14 16:39:08 Next: 1 seconds after
2017/12/14 16:39:09 Kill: khubd(PID:23 PPID:2) - by monkey
2017/12/14 16:39:09 Next: 1 seconds after
2017/12/14 16:39:10 Kill: scsi_eh_1(PID:333 PPID:2) - by monkey
2017/12/14 16:39:10 Next: 1 seconds after
2017/12/14 16:39:11 Kill: acpid(PID:1156 PPID:1) - by monkey
2017/12/14 16:39:11 Next: 1 seconds after
2017/12/14 16:39:12 Kill: mingetty(PID:23603 PPID:1) - by monkey
2017/12/14 16:39:12 Next: 1 seconds after
2017/12/14 16:39:13 Kill: hald(PID:1165 PPID:1) - by monkey
2017/12/14 16:39:13 Next: 1 seconds after
2017/12/14 16:39:14 Kill: rsyslogd(PID:1013 PPID:1) - by monkey
2017/12/14 16:39:14 Next: 1 seconds after
2017/12/14 16:39:15 Kill: khungtaskd(PID:28 PPID:2) - by monkey
2017/12/14 16:39:15 Next: 1 seconds after
Connection to 127.0.0.1 closed by remote host.
Connection to 127.0.0.1 closed.

最後にはsshdが殺されてセッションが切られました。
現実でこんなにプロセスが死んでたら悪夢ですね。

外部トリガー動作

Nightmare MonkeyはRESTのAPIとして動作させており、外部から悪夢を引き起こすことも出来ます。

KILLコマンドを発行するためには、{{HOST}}/api/killというURLにPOSTを投げます。
この際、メモ的に使えるdescパラメータを必須にしています。
※ この辺は特に設計もせずコードを書いているので突っ込まないでください。

以下はNightmare MonkeyにプロセスをKillするリクエストを送り込んでいる例です。

KILLリクエストをcurlで送る
$ curl http://{{HOST_ADDR}}:8080/api/kill -X POST -d '{"desc":"I like kill!"}'

成功すると何を殺したかの応答が返ってきます。

KILL応答例
{"name":"crond","desc":"I like kill!","pid":1966,"ppid":1}

今回はcrondが死にました。
障害対応の訓練という用途では、この応答はそのまま捨てるのがお勧めです。
あるいはログに留め、答え合わせに利用しましょう。

この例を見ていてわかると思いますが、 認証もセキュリティもあったもんじゃありません。
本番環境で動作はさせないでくださいね。

活用例

さて、ここまでは駆け足でNightmare Monkeyの実装の紹介をしてきました。
さぁここからがアドベントカレンダーです。

外部トリガー実装例(1)

hubotにこんなコードを入れておきましょう。

チャットからの接続の実装例
  robot.hear /(destroy)|(ですとろい)|(デストロイ)/, (msg) ->
    data = JSON.stringify({
      desc: "request from hubot"
    })
    robot.http("http://{{HOST_ADDR}}:8080/api/kill")
      .header('Content-Type', 'application/json')
      .post(data) (error, response, body) ->
      msg.send "(emoticon)< デストイイイ!!!!"

これでチャットで「destroy」と呟くたびにプロセスが死んでいきます。

nightmare-monkeyのログ
2017/12/16 11:54:49 Kill: drbd0_submit(PID:1484 PPID:2) - request from hubot

おっと、「destroy」なんて普段発しない言葉でしたね。( しかもタイポしてますね! )
どうせなら「出社」に反応するようにして朝から悪夢を見せるのも良いし、「おつかれさま」に反応して疲れてる人に追い討ちを掛けるのも良いですね。

外部トリガー実装例(2)

BASHでこんなコードを書き、金曜日に飲みに行く前に慌ててCronに「月曜日から動くように」仕込んでおきました。

コード

トリガー
#!/bin/bash -
URL="https://qiita.com/api/v2/items/c31be8235354a3e21945"
TMPFILE="/var/tmp/tmp.next"
if [ -f ${TMPFILE} ]; then
  NEXT=$(cat ${TMPFILE})
else
  NEXT=3
fi

LIKE=$(curl ${URL} 2>/dev/null | jq .likes_count)
[[ ${LIKE} == "null" ]] && exit 0

if [[ ${LIKE} -ge ${NEXT} ]]; then
  echo "$(( ${NEXT} + 3 ))" > ${TMPFILE}
  curl http://localhost:8080/api/kill -X POST -d '{"desc":"Congraturations you have got '${LIKE}' like"}' 2>/dev/null
fi

さて、これは何でしょうか。

解説

2行目にURLが見えますね。
Qiita API経由の、この記事のURLです。

下から2行目にどこかで見たcurlが見えますね。
localhostで動いているNightmare MonkeyへのKILLリクエストです。

その前にif条件がありますね。
簡単に言うと、3,6,9,...と、3いいね毎に条件がtrueになります。

以上から、この記事のいいねが増えるたびに、とあるサーバーでNightmare Monkeyが悪夢を起こします。

つまり

押すなよ!絶対に押すなよ!

最後に

と言うわけで、己研鑽のために動化された己破壊的なプログラムを作った話でした。

  • 絶対に本番で動かさないでくださいね
  • さすがに上司に怒られるので、「とあるサーバー」は本番ではありません。
  • 慌てて書いたコードなので色々変ですがご容赦を。
  • 今後機能をもっと実装して行きたいと思ってます。
  • いいねのトリガーが無事に動いたら、追記しますね。
  • こんなにクリスマス前に「殺す」とか「死ぬ」とか「悪夢」とか書いているの俺だけじゃないか? ( BANされないよね? )

追記: 顛末その1

この投稿の2日後、月曜日朝時点で「30いいね」頂きました。
ありがとうございます。

そしてCron設定された時間を迎え、トリガーはちゃんと動作しました。

log
2017/12/18 11:00:02 Kill: kworker/1:2H(PID:13669 PPID:2) - Congraturations you have got 30 like
2017/12/18 11:10:02 Kill: agetty(PID:1308 PPID:1) - Congraturations you have got 30 like
2017/12/18 11:20:02 Kill: ksmd(PID:40 PPID:2) - Congraturations you have got 30 like
2017/12/18 11:30:02 Kill: sshd(PID:23713 PPID:23711) - Congraturations you have got 30 like
2017/12/18 11:40:02 Kill: drbd_r_r0(PID:1507 PPID:2) - Congraturations you have got 30 like
2017/12/18 11:50:01 Kill: kdmflush(PID:685 PPID:2) - Congraturations you have got 30 like
2017/12/18 12:00:02 Kill: arc_reclaim(PID:1540 PPID:2) - Congraturations you have got 30 like
2017/12/18 12:10:02 Kill: rpciod(PID:484 PPID:2) - Congraturations you have got 30 like
2017/12/18 12:20:02 Kill: drbd0_submit(PID:1484 PPID:2) - Congraturations you have got 30 like
2017/12/18 12:30:05 Kill: xfs-buf/vda1(PID:637 PPID:2) - Congraturations you have got 30 like

しかし・・・10回のKILLが走りましたが、まだこれと言って面白い障害は発生していません。
中途半端なので、引き続きこのまま放置しておきます。