AWSからメンテ通知が来たら５秒でインスタンスを再起動させる方法（Slackメッセージボタンでの事前確認付き）

AWSから不定期に届くメンテ通知や障害メールの対応を、PCやブラウザを使わずにSlackで完結させます。

## 完成図
できあがったものがこれです。

* 作成所要時間　約15分
![2017-11-20 10_00_16-aws-mentenance.json - FrontOps.png](https://qiita-image-store.s3.amazonaws.com/0/133603/d5033e76-7d9f-4aaa-42ad-c75d05ab4be7.png)

※定型対応フローが簡単に作れるアプリ[FrontOps](https://frontops.exhands.org) を使っています。

## はじめに

ある日、こんな件名のメールが届きます。

* Amazon EC2 Maintenance - Maintenance
* [Retirement Notification]

インスタンスが動いている物理サーバをメンテするから、いつまでにインスタンスを移動してください。
インスタンスの状態が異常なので、インスタンスを確認して再起動してください。

といった内容です。
このメール、たまにしか来ないことに加え、英語なこともあり他の迷惑メールや広告メールに紛れて見落とすことがよくありました。

そこで、このメールを受信したらSlackに通知させて見落としを防止することにしました。


またこのメール。メール文中にはインスタンスID の記載しかないので、インスタンス名を確認しないと影響判断ができません。
ただ、インスタンス名を確認するにはいちいちAWSのマネジメントコンソールにログインする必要があって面倒です。

そこでSlack通知の際に、インスタンス名を補足させることにします。

また、このメールの対応としてはインスタンスを再起動するしかありません。
機械的に再起動させてもいいのですが、
再起動前に事前作業が必要なインスタンスもあるので、
事前にSlackのメッセージボタンを使って承認を挟むことにします。


## 必要なもの
* FrontOps (https://frontops.exhands.org)
    * 無料アカウントで、アプリをダウンロードして利用することができます
* AWS CLIが使えてSSHでログインできるサーバ
    * あらかじめ`aws configure`でクレデンシャル等は設定しておきます。
* 通知先のSlackチャンネル

## 設定説明
全体フローは冒頭にあげた通りです。以下では各ノードの設定を説明します。

### メール定期受信
メールを定期的に受信させます。1分ごとにしました。
![2017-11-21 08_15_52-aws-mentenance.json - FrontOps.png](https://qiita-image-store.s3.amazonaws.com/0/133603/7c0f78fe-5d12-aa52-cdf1-dec1dd124811.png)

### 件名でフィルタ
メールの件名は、`msg.topic`に入ってきます。`msg.topic`に文字列が含まれるかどうかを設定します。
条件に該当する場合は次のノードに進みます。
条件に該当しない場合はここで処理が終わります。
![2017-11-20 23_48_12-aws-mentenance.json - FrontOps.png](https://qiita-image-store.s3.amazonaws.com/0/133603/99f5833b-0653-3b04-577b-277bea92b0a4.png)

### インスタンスID取得
メールの本文は、`msg.payload`に入ってきます。
ここからJSONATAを使ってインスタンスIDを抽出して、`msg.instanceId`に設定します。
```
$match(payload, /^(i-\w+)/m).groups
```
![2017-11-21 07_41_19-aws-mentenance.json - FrontOps.png](https://qiita-image-store.s3.amazonaws.com/0/133603/4caad717-75e9-4dcf-a6ca-57472405fb53.png)

### インスタンス名取得
インスタンス名は、ふだん使っているサーバにSSHログインしてAWS CLIで取得します。

取得した結果は、`msg.instanceName`に設定します。
コマンド部にはmustacheが使えるので、`{{{プロパティ}}}`で`msg`オブジェクトのプロパティを展開できます。
ここでは先に取得したインスタンスIDを展開させています。

```
aws ec2 describe-instances --instance-id {{{instanceId}}} --query 'Reservations[*].Instances[*].[InstanceId,State.Name,Tags[?Key==`Name`].Value]' --output text
```

* サーバへの接続設定は [サーバにSSHログインしてコマンドを実行する方法](https://youtu.be/lfjOm9ZzMHo)を参照してください。

![2017-11-20 23_48_51-aws-mentenance.json - FrontOps.png](https://qiita-image-store.s3.amazonaws.com/0/133603/5415eb92-e665-5ebb-dc5b-cc653d6e9de5.png)

### Slack通知
Slackの`general`チャンネルに

メールの件名(`msg.topic`)
メールの本文(`msg.payload`)
インスタンス名(`msg.instanceName.stdout`)

を通知します。

* Slackの設定は[Slackでメッセージを送受信する方法](https://youtu.be/LNpkfCBHlaw)を参照してください

![2017-11-20 23_49_09-aws-mentenance.json - FrontOps.png](https://qiita-image-store.s3.amazonaws.com/0/133603/dfe856b6-b54d-69ee-c15d-015d1800f18e.png)

### 再起動実施確認
インスタンスの停止・起動をメッセージボタンで確認します。
よく使うYes/Noの形式なので、情報ウィンドウに表示されているテンプレートをコピペして文言だけ直して使います。

* メッセージボタンの応答を受け付けるにはSlack側の設定が必要です。[Slackメッセージボタンを使って対話する方法](https://youtu.be/Di1hn5MhTBQ)を参照してください

![2017-11-20 23_49_28-aws-mentenance.json - FrontOps.png](https://qiita-image-store.s3.amazonaws.com/0/133603/5e95d954-593a-90b5-6676-90b588bb9d91.png)

### 復唱
どのメッセージボタンが押されたかを復唱させます。この情報は`msg.answer`に入ってきます。
これもよくあるパターンなので、情報ウィンドウに表示されているテンプレートをコピペして使います。
![2017-11-20 23_49_41-aws-mentenance.json - FrontOps.png](https://qiita-image-store.s3.amazonaws.com/0/133603/9fb0e778-00b5-8c47-8869-ce5a09f87947.png)

### 承認結果判定
ボタン操作の結果に応じて分岐を作ります。
![2017-11-20 23_49_55-aws-mentenance.json - FrontOps.png](https://qiita-image-store.s3.amazonaws.com/0/133603/a36a2aed-34d9-b1cf-8bc7-3246115b6b9f.png)

### 対応終了
![2017-11-20 23_50_10-aws-mentenance.json - FrontOps.png](https://qiita-image-store.s3.amazonaws.com/0/133603/246fc788-4847-78b2-c8e4-bfc3785d45ba.png)

### 対応開始宣言
![2017-11-20 23_50_23-aws-mentenance.json - FrontOps.png](https://qiita-image-store.s3.amazonaws.com/0/133603/13d7a133-6c8c-4084-5fef-23c6f2a34dd6.png)

### インスタンス停止
インスタンス停止は、AWS CLIでおこないます。
インスタンスを停止させて、ステータスがstoppedになるまで待ちます。
タイムアウトは60秒にしました。
![2017-11-20 23_52_15-aws-mentenance.json - FrontOps.png](https://qiita-image-store.s3.amazonaws.com/0/133603/5d6a28c7-c619-e1c9-91e7-ca53693eac77.png)

### インスタンス起動
インスタンス起動は、AWS CLIでおこないます。
インスタンスを起動させて、ステータスがrunningになるまで待ちます。
タイムアウトはちょっと長めに90秒にしておきます。
![2017-11-21 08_09_04-aws-mentenance.json - FrontOps.png](https://qiita-image-store.s3.amazonaws.com/0/133603/e800dbfc-68fb-33eb-acf3-c73a20842f95.png)

### 対応完了報告
対応完了を報告させて終了します。
この前にさらに正常性確認を追加してもいいかもしれません。
![2017-11-20 23_51_24-aws-mentenance.json - FrontOps.png](https://qiita-image-store.s3.amazonaws.com/0/133603/1ee1b01a-48ba-b2b5-8833-332812fe53f3.png)

### 例外処理
タイムアウトやその他エラーが発生した時には、ログに記録を残させると同時にSlackに通知させています。
電話で通知させるパターンもよくあります。


## フローデータ
以下をコピーして、[読み込み]＞[クリップボード] でフローを取り込むことができます。

```
[{"id":"67e11027.4cc2c","type":"command","z":"bd84d76c.2d93f8","connectionconfig":"fd7512e2.f22ec","command":"aws ec2 describe-instances --instance-id {{{instanceId}}} --query 'Reservations[*].Instances[*].[InstanceId,State.Name,Tags[?Key==`Name`].Value]' --output text","timer":"15","oldrc":false,"env":"","pty":false,"trim":false,"field":"instanceName","fieldType":"msg","name":"インスタンス名取得","x":190,"y":240,"wires":[["b0addf6c.85c41"]]},{"id":"10686c8e.7ab4d3","type":"e-mail in","z":"bd84d76c.2d93f8","name":"メール定期受信","protocol":"IMAP","server":"imap.gmail.com","useSSL":true,"port":"993","box":"INBOX","disposition":"Read","repeat":"60","x":130,"y":160,"wires":[["20849217.8d4fae"]]},{"id":"20849217.8d4fae","type":"switch","z":"bd84d76c.2d93f8","name":"件名でフィルタ","property":"topic","propertyType":"msg","rules":[{"t":"cont","v":"Amazon EC2 Maintenance - Maintenance","vt":"str"},{"t":"cont","v":"[Retirement Notification]","vt":"str"}],"checkall":"true","outputs":2,"x":310,"y":160,"wires":[["dfb73593.bb2f08"],["dfb73593.bb2f08"]]},{"id":"dfb73593.bb2f08","type":"change","z":"bd84d76c.2d93f8","name":"インスタンスID取得","rules":[{"t":"set","p":"instanceId","pt":"msg","to":"$match(payload, /^(i-\\w+)/m).groups[0]\t","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":510,"y":160,"wires":[["67e11027.4cc2c"]]},{"id":"b0addf6c.85c41","type":"slack-say","z":"bd84d76c.2d93f8","slack":"","channel":"general","msg":"*{{{topic}}}*\n\n```\n{{{payload}}}\n```\n{{{instanceName.stdout}}}","output":"str","gonext":true,"name":"Slack通知","x":380,"y":240,"wires":[["f6e76317.5bd3"]]},{"id":"f6e76317.5bd3","type":"slack-ask","z":"bd84d76c.2d93f8","slack":"","channel":"general","msg":"{\n  \"text\": \"インスタンスを停止・起動していいですか？\",\n  \"attachments\": [\n    {\n      \"title\": \"承認依頼\",\n      \"text\": \"実行してよろしいですか\",\n      \"fallback\": \"このクライアントでは返答できません\",\n      \"callback_id\": \"approve_123\",\n      \"color\": \"#3AA3E3\",\n      \"attachment_type\": \"default\",\n      \"actions\": [\n        {\n          \"name\": \"answer\",\n          \"text\": \"承認\",\n          \"value\": \"yes\",\n          \"type\": \"button\",\n          \"style\": \"primary\"\n        },\n        {\n          \"name\": \"answer\",\n          \"text\": \"否認\",\n          \"value\": \"no\",\n          \"type\": \"button\"\n        }\n      ]\n    }\n  ]\n}","timeout":"30","timeoutUnits":"minutes","name":"再起動実施確認","x":200,"y":300,"wires":[["225facba.2a5cf4"]]},{"id":"225facba.2a5cf4","type":"slack-say","z":"bd84d76c.2d93f8","slack":"","channel":"general","msg":"> {{{payload.original_message.text}}}\n{{{answer.value}}} by {{{payload.user.name}}}","output":"str","gonext":true,"name":"復唱","x":370,"y":300,"wires":[["75b36d.ac831c94"]]},{"id":"75b36d.ac831c94","type":"switch","z":"bd84d76c.2d93f8","name":"承認結果判定","property":"answer.value","propertyType":"msg","rules":[{"t":"eq","v":"no","vt":"str"},{"t":"eq","v":"yes","vt":"str"}],"checkall":"true","outputs":2,"x":540,"y":300,"wires":[["eb240803.79d998"],["351ce544.8e522a"]]},{"id":"3c6efba0.b994e4","type":"catch","z":"bd84d76c.2d93f8","name":"例外処理","scope":["b0addf6c.85c41","75b36d.ac831c94","dfb73593.bb2f08","cd9f1df.a3aede","67e11027.4cc2c","c274bafd.cddfe8","20849217.8d4fae","f6e76317.5bd3","10cc5de3.b72572","eb240803.79d998","351ce544.8e522a","225facba.2a5cf4","b4948d24.29e29"],"x":160,"y":460,"wires":[["a640fbbb.d2ee18","25f25187.2b4cbe","6fdbe105.9ed53"]]},{"id":"eb240803.79d998","type":"slack-say","z":"bd84d76c.2d93f8","slack":"","channel":"general","msg":"後の対応をお願いします。","output":"str","gonext":false,"name":"対応終了","x":800,"y":300,"wires":[]},{"id":"351ce544.8e522a","type":"slack-say","z":"bd84d76c.2d93f8","slack":"","channel":"general","msg":"対応を始めます","output":"str","gonext":true,"name":"対応開始宣言","x":200,"y":360,"wires":[["cd9f1df.a3aede"]]},{"id":"cd9f1df.a3aede","type":"command","z":"bd84d76c.2d93f8","connectionconfig":"fd7512e2.f22ec","command":"aws ec2 stop-instances --instance-id {{{instanceId}}}\naws ec2 wait instance-stopped --instance-id {{{instanceId}}}","timer":"60","oldrc":false,"env":"","pty":false,"trim":false,"field":"payload","fieldType":"msg","name":"インスタンス停止","x":400,"y":360,"wires":[["b4948d24.29e29","c274bafd.cddfe8"]]},{"id":"b4948d24.29e29","type":"slack-say","z":"bd84d76c.2d93f8","slack":"","channel":"general","msg":"インスタンスを停止しました。\nインスタンスを起動します。","output":"str","gonext":false,"name":"途中報告","x":600,"y":400,"wires":[]},{"id":"c274bafd.cddfe8","type":"command","z":"bd84d76c.2d93f8","connectionconfig":"fd7512e2.f22ec","command":" aws ec2 start-instances --instance-id {{{instanceId}}}\naws ec2 wait instance-running --instance-id {{{instanceId}}}","timer":"90","oldrc":false,"env":"","pty":false,"trim":false,"field":"payload","fieldType":"msg","name":"インスタンス起動","x":620,"y":360,"wires":[["10cc5de3.b72572"]]},{"id":"10cc5de3.b72572","type":"slack-say","z":"bd84d76c.2d93f8","slack":"","channel":"general","msg":"インスタンスを起動しました。\n対応が完了しました。","output":"str","gonext":false,"name":"対応完了報告","x":820,"y":360,"wires":[]},{"id":"a640fbbb.d2ee18","type":"slack-say","z":"bd84d76c.2d93f8","slack":"","channel":"general","msg":"異常が発生しました。対応をお願いします。\n{{{error.message}}}\n```\n{{{payload}}}\n```\n","output":"str","gonext":false,"name":"異常発生連絡","x":400,"y":460,"wires":[]},{"id":"25f25187.2b4cbe","type":"debug","z":"bd84d76c.2d93f8","name":"","active":true,"console":"false","complete":"true","x":370,"y":560,"wires":[]},{"id":"6fdbe105.9ed53","type":"file","z":"bd84d76c.2d93f8","name":"ログ追記","filename":"c:\\Temp\\aws-mentenance.log","appendNewline":true,"createDir":true,"overwriteFile":"false","x":377.1000061035156,"y":510.20001220703125,"wires":[]},{"id":"d20cc86f.3c0dd8","type":"comment","z":"bd84d76c.2d93f8","name":"AWSのメンテ通知の対応","info":"","x":140,"y":100,"wires":[]},{"id":"fd7512e2.f22ec","type":"connection","z":"","name":"util server","host":"35.185.201.106","username":"sakazuki"}]
```

## 最後に
[FrontOps](https://frontops.exhands.org) を使うと、複数のツール・ソフトを簡単につなげて制御することができます。
少人数でシステム運用・保守をされている方に是非活用いただきたいアプリです。