こんにちは。AWS担当のwakです。今回はAWS監視の話をします。
本文とは特に関係のないかわいい猫
CloudWatchのおさらい
AWSにはCloudWatchという監視サービスが用意されています。CPU使用率やDBコネクション数といった監視条件を事前に設定しておくと、EC2やRDSといったサービスが
- この条件を満たしたとき(ALARM)
- この条件を満たさなくなったとき (OK)
- 判定する情報が不足しているとき (INSUFFICIENT_DATA)
のタイミングで何らかの通知を行うことができます。また、アプリケーションのログ、top
df
などの結果をCloudWatchに送信して、監視・通知を代行してもらうこともできます(カスタムメトリクス)。
SNS……CloudWatchの通知先として
ではその通知はどこに行くのでしょうか。それを知るにはまずAmazon SNSについての説明が必要です。
AWSには、SNS (Simple Notification Service)というメッセージングサービスがあります。
誰かがボタンを押すと(このボタン1個をトピックと呼びます)、あらかじめ登録してあった通知先にメッセージが送信されるだけのシンプルなサービスです。送信先にはEメールアドレス、WebサービスのエンドポイントのURL、モバイルプッシュ通知*1、そして後述のSQSが指定できます。
CloudWatchの通知先にはこのSNSを指定します。すると、
- EC2で障害が発生する
- CloudWatchが障害を検出して、指定されたトピックを叩く
- トピックは事前に登録されていたメールアドレスに通知を送信する
- 担当者がメールを受け取って復旧作業を行う
といった自動通知処理が実現できるわけです。
SQS……SNSの通知先として
AWSにはSQS (Simple Queue Service) というサービスが用意されています。これは名前の通り、メッセージを入れたり出したりするためのキューを提供します。キューは単独で使うこともできて、たとえばシナリオはこんな風になります。
- データを吐くシステムAと、そのデータを処理するシステムBがある
- システムAは不定期にデータを出力し、そのデータを書いたメッセージをキューに格納する
- システムBは暇になったときキューからメッセージを取得し、そこに書いてあるデータを処理する(処理したらメッセージを削除する)
- AとBは直接メッセージのやり取りはしないが、協調して非同期に動作することができる
「メッセージ」はプレーンテキストで記述された任意のデータです。最大サイズは256KBまで。多くの用途には十分なサイズです。
Slack APIと繋げると
弊社で利用しているチャットシステム・SlackはAPIが充実しており、HTTP POSTで送信した内容をそのままチャットでbotに発言させるためのAPIがあります。この仕組みを使うと、簡単なバッチを走らせるだけでCloudWatchのアラートをSlackに送信することができます。
- CloudWatchは警告発生時にSNSのトピックへメッセージを送信する
- そのトピックはSQSのキューにメッセージを格納する(ここまではAWSのコンソールから設定可能)
- EC2上に小さなバッチを用意する。このバッチは無限ループを回り、キューを監視する
- キューにメッセージが入ってくると、バッチはSlackにその旨を通知する
実にシンプルですね。
やってみる
というわけで、簡単に手順を書いてみます。今回はバッチにrubyを使いますが、PHPでもPowerShell(!!)でも手順はあまり変わりません。大まかな手順は次の通りです。 1. AWS APIを叩くためのユーザーと認証用キーを用意する 1. SQSのキューを作成する 1. SNSのトピックを作成してSQSとつなげる 1. CloundWatchのメトリクスを作成してSNSとつなげる 1. SQSを監視するスクリプトを用意する
API用のIAM Userを用意する
存在しなければ新しく作りましょう。ユーザー詳細画面のPermissionsタブの中にInline Policiesメニューがあるので、Policy Generatorで権限を作成します。Amazon SQSの下にある「Delete Message」「ReceiveMessage」だけ選択しておけば良いでしょう。
Add Statementボタンを押すのを忘れないように
SQSを用意する
SQSで新しいキューを作成します。Create New Queueを選び、設定(まあデフォルトで良いでしょう)を決めたらすぐに作成されます。
この時点でエンドポイントとARNが決まります
キューを作成すると、コンソールからテストメッセージを(手書きで)SQSへ送信したり、キューに入っているメッセージを確認・削除したりできます。右クリックメニューのSend a Messageを選択するとメッセージ送信ウィンドウが開きます。これをrubyで取得してみましょう。
gem install aws-sdk
でSDKがインストールできるので、その後は次のようなコードを実行するだけです。ロングポーリングを勝手にやってくれるので、自分でウェイトをかけたりする必要はありません。
require 'aws-sdk' # 認証用キーのデフォルト値を設定する。今後APIを叩くたび毎回この値が送信される Aws.config = {:access_key_id => '<アクセスキー>', :secret_access_key => '<シークレットキー>', :region => 'ap-northeast-1'} # キューにアクセスするためのインスタンスを取得 poller = Aws::SQS::QueuePoller.new('https://sqs.ap-northeast-1.amazonaws.com/999999999999/HelloSQS') poller.poll(:wait_time_seconds => 20, :idle_timeout => 5) {|msg| p msg}
実行結果はこんな感じでした。「HELLO WORLD!」がコンソールから入力した値です。またpoll
メソッドの戻り値として、メッセージの取得件数、最終メッセージのタイムスタンプなどが取れます。また、poll
で受信したメッセージは自動的に削除されるため、明示的な削除は不明です(この動作は:skip_delete
にfalseを指定してスキップできます)。
irb(main):013:0> poller.poll(:wait_time_seconds => 20, :idle_timeout => 5) {|msg| p msg} #<struct message_id="……", receipt_handle="……", md5_of_body="……", body="HELLO WORLD!", attributes={"SenderId"=>"……", "ApproximateFirstReceiveTimestamp"=>"1427414334046", "ApproximateReceiveCount"=>"1", "SentTimestamp"=>"1427414334044"}, md5_of_message_attributes=nil, message_attributes={}>
キューにパーミッションを追加する
現時点ではこのキューにメッセージを送ることができるのは自分だけです。SNSからメッセージを送信することができるように、Add a Permissionメニューからパーミッションを追加しておきましょう。
エンドポイントを知っていればどこからでもこのキューにメッセージを投げ込むことができるようになる
SNSを用意する
次はSNSです。Create New Topicでトピックを作成したら、Create Subscriptionで購読設定の追加を行います。ついでに自分のメールアドレスを追加しても結構です。(メールアドレスの確認作業が必要になるため手順は省略します)
これでSNSとSQSがつながったことになります。SNSのトップに戻り、Publish to topicをクリックしてテストメッセージを送信してみましょう。
これをさっきのrubyのコードで取得してみます。結果のうち、body
プロパティの値を示します。JSONエンコードされているのでちょっと整形しました。
{ "Type" : "Notification", "MessageId" : "77c5f3c4-679b-5931-aed7-be4f5619a1d2", "TopicArn" : "arn:aws:sns:ap-northeast-1:999999999999:HelloSNS", "Subject" : "HELLO SNS!", "Message" : "HELLO I AM NEKO", "Timestamp" : "2015-03-24T09:32:50.814Z", "SignatureVersion" : "1", "Signature" : "wZMZ....g==", "SigningCertURL" : "https://....amazonaws.com/SimpleNotificationService-....pem", "UnsubscribeURL" : "https://....amazonaws.com/?Action=Unsubscribe&SubscriptionArn=...", "MessageAttributes" : { "AWS.SNS.MOBILE.MPNS.Type" : {"Type":"String","Value":"token"}, "AWS.SNS.MOBILE.WNS.Type" : {"Type":"String","Value":"wns/badge"}, "AWS.SNS.MOBILE.MPNS.NotificationClass" : {"Type":"String","Value":"realtime"} } }
CloudWatchを用意する
AWS側はこれで最後です。CloudWatchのメトリクスを作成します。
Descriptionは後から好きに変更できますが、Nameは一度作成したら変えられません。また、現状ではメトリクスにタグを付けることもできないので、監視内容のレベルに応じて「WARN」や「ERROR」といったプレフィックスを付けておくと良いかもしれません。図ではステータスがALARMになったときのみ通知を行っていますが、OKになったときにも通知を行うこともできます。
作成したらテストをしてみます。次のコードで、作成したメトリクスのコードをむりやりALARMにセットしてみます(これはテスト用コマンドなので、現在の値(正常値)にあわせて勝手に元に戻ります)。
cloudwatch = Aws::CloudWatch::Client.new(region: 'ap-northeast-1') cloudwatch.set_alarm_state(alarm_name: "WARN_HelloCloudWatch", state_value: "ALARM", state_reason: "THIS IS NOT A DRILL")
こういう1回限りの操作はPowerShellでやってもいいです。
PS C:\>Initialize-AWSDefaults -accessKey '<アクセスキー>' -secretKey '<シークレットキー>' -Region ap-northeast-1 PS C:\>Set-CWAlarmState -AlarmName WARN_HelloCloudWatch -StateValue ALARM -StateReason "THIS IS NOT A DRILL"
メトリクスのステータスがALARMに変更され、SNS→SQSとメッセージが送信されてゆきます。
Slack botの準備をする
Slackへ何か送信するためには、事前にIncoming Webhookの設定画面でもらえるトークンが必要です。トークンがあれば、次の簡単なコードで送信ができます。
def saySomething(channel, text, username = nil, emoji = nil) token = '<slackのトークン>' uri = 'https://your-slack-domain-name.slack.com/services/hooks/incoming-webhook' params = {:channel => channel, :text => text, :username => 'SlackBot'} params[:username] = username if username params[:icon_emoji] = emoji if emoji query = {'payload' => params.to_json}.map{|k, v| "#{k}=#{v}"}.join('&') uri_parsed = URI.parse(uri) http = Net::HTTP.new(uri_parsed.host, 443) http.use_ssl = true http.post(uri_parsed.path + '?token=' + token, URI.escape(query)).body end
text
は発言内容です。改行は\r\n
を使ってください。channel
は送信先チャンネル名で、'chanelName'
,'#privateGroupName'
,'@userName'
のように指定します。スペルミスがあるとそのメッセージは闇に飲まれて消えてしまいます。emoji
はbotのアイコンを指定するための値で、':heart:'
のようにコロンでくくった形で渡します。
つなげる
後は無限ループの中でキューをポーリングし、何かメッセージがあったらSlackにそのまま送信するだけです。
require 'aws-sdk' Aws.config = {:access_key_id => '<アクセスキー>', :secret_access_key => '<シークレットキー>', :region => 'ap-northeast-1'} poller = Aws::SQS::QueuePoller.new('https://sqs.ap-northeast-1.amazonaws.com/999999999999/HelloSQS') channelName = 'your_channel_name' while true poller.poll(:wait_time_seconds => 20, :idle_timeout => 5) {|msg| begin body = JSON.parse(msg.body) message = JSON.parse(body['Message']) rescue => e saySomething(channelName, "電文のパースに失敗しました: " + msg.body) return false end if message == nil || message['NewStateValue'] == nil || message['AlarmDescription'] == nil saySomething(channelName, "なんか変なメッセージが来てます: " + msg.body) return false end text = '' if message['NewStateValue'] == 'OK' saySomething(channelName, "[復帰] CloudWatchのアラートが解消しました。\r\n" + "Name : " + message['AlarmName'] + "\r\n" + "Descrpition : " + message['AlarmDescription'] + "\r\n" + "Reason : " + message['NewStateReason'], "よかったね", ":heart:") else saySomething(channelName, "[警告] CloudWatchのアラートが発生しました。\r\n" + "Name : " + message['AlarmName'] + "\r\n" + "Descrpition : " + message['AlarmDescription'] + "\r\n" + "Reason : " + message['NewStateReason'], "たいへんです", ":broken_heart:") end } end
Slackにこうして通知が来ます。
まとめ
エラー通知は大切ですが、受け取る側が気付かないのでは意味がありません。このSlack連携を導入して以来、エラー発生から対応までの時間が激減(多くの場合は即時)しました。 これだけ簡単な設定とバッチで通知システムができるのは楽しいですし、(得られる価値÷書いたコードの行数)は自分の仕事人生の中でも最大値を記録しました。
今回はSlackに通知をするようにしましたが、重要度に応じて携帯電話に音声発信をするTwillioのAPIを呼べば電話通知もできるようになります。 これからもなるべく手抜きをして最大の成果が得られるように頑張っていこうと思います。