AWSから不定期に届くメンテ通知や障害メールの対応を、PCやブラウザを使わずにSlackで完結させます。
完成図
できあがったものがこれです。
※定型対応フローが簡単に作れるアプリFrontOps を使っています。
はじめに
ある日、こんな件名のメールが届きます。
- Amazon EC2 Maintenance - Maintenance
- [Retirement Notification]
インスタンスが動いている物理サーバをメンテするから、いつまでにインスタンスを移動してください。
インスタンスの状態が異常なので、インスタンスを確認して再起動してください。
といった内容です。
このメール、たまにしか来ないことに加え、英語なこともあり他の迷惑メールや広告メールに紛れて見落とすことがよくありました。
そこで、このメールを受信したらSlackに通知させて見落としを防止することにしました。
またこのメール。メール文中にはインスタンスID の記載しかないので、インスタンス名を確認しないと影響判断ができません。
ただ、インスタンス名を確認するにはいちいちAWSのマネジメントコンソールにログインする必要があって面倒です。
そこでSlack通知の際に、インスタンス名を補足させることにします。
また、このメールの対応としてはインスタンスを再起動するしかありません。
機械的に再起動させてもいいのですが、
再起動前に事前作業が必要なインスタンスもあるので、
事前にSlackのメッセージボタンを使って承認を挟むことにします。
必要なもの
- FrontOps (https://frontops.exhands.org)
- 無料アカウントで、アプリをダウンロードして利用することができます
- AWS CLIが使えてSSHでログインできるサーバ
- あらかじめ
aws configure
でクレデンシャル等は設定しておきます。
- あらかじめ
- 通知先のSlackチャンネル
設定説明
全体フローは冒頭にあげた通りです。以下では各ノードの設定を説明します。
メール定期受信
件名でフィルタ
メールの件名は、msg.topic
に入ってきます。msg.topic
に文字列が含まれるかどうかを設定します。
条件に該当する場合は次のノードに進みます。
条件に該当しない場合はここで処理が終わります。
インスタンスID取得
メールの本文は、msg.payload
に入ってきます。
ここからJSONATAを使ってインスタンスIDを抽出して、msg.instanceId
に設定します。
$match(payload, /^(i-\w+)/m).groups
インスタンス名取得
インスタンス名は、ふだん使っているサーバに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ログインしてコマンドを実行する方法を参照してください。
Slack通知
Slackのgeneral
チャンネルに
メールの件名(msg.topic
)
メールの本文(msg.payload
)
インスタンス名(msg.instanceName.stdout
)
を通知します。
- Slackの設定はSlackでメッセージを送受信する方法を参照してください
再起動実施確認
インスタンスの停止・起動をメッセージボタンで確認します。
よく使うYes/Noの形式なので、情報ウィンドウに表示されているテンプレートをコピペして文言だけ直して使います。
- メッセージボタンの応答を受け付けるにはSlack側の設定が必要です。Slackメッセージボタンを使って対話する方法を参照してください
復唱
どのメッセージボタンが押されたかを復唱させます。この情報はmsg.answer
に入ってきます。
これもよくあるパターンなので、情報ウィンドウに表示されているテンプレートをコピペして使います。
承認結果判定
対応終了
対応開始宣言
インスタンス停止
インスタンス停止は、AWS CLIでおこないます。
インスタンスを停止させて、ステータスがstoppedになるまで待ちます。
タイムアウトは60秒にしました。
インスタンス起動
インスタンス起動は、AWS CLIでおこないます。
インスタンスを起動させて、ステータスがrunningになるまで待ちます。
タイムアウトはちょっと長めに90秒にしておきます。
対応完了報告
対応完了を報告させて終了します。
この前にさらに正常性確認を追加してもいいかもしれません。
例外処理
タイムアウトやその他エラーが発生した時には、ログに記録を残させると同時に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 を使うと、複数のツール・ソフトを簡単につなげて制御することができます。
少人数でシステム運用・保守をされている方に是非活用いただきたいアプリです。