AWSを使ってのサーバの死活監視ってよくやると思うんですが、大体問題があった時の通知の飛ばし先はSlackだったりします。(無料だし)
とはいえ仕事で使おうとすると、Slackは見れない状況もあるかもしれないと思い、メールで通知を飛ばす、という観点でAWSを利用し実現してみました。
最終的に利用できるようにしたものは以下に登録してあります。
#サーバ監視からのメール送付までの仕組み
公開されているサービスに対してREST通信を行い、そのステータスコードを見るだけのシンプルな死活監視を行います。
ただし何も考えずに実装すると、エラーが起きてから回復するまで、定期実行のたびに何度もエラーメールを投げてしまう仕組みとなるため、管理する側としてはうっとおしくもあり(何回もメールが来る)コスト的にも嬉しくありません。またエラー発生時だけメールを飛ばす仕組みとすると、復旧したかどうかがわからず、管理者はヒヤヒヤすることになります。
そこで、現在のステータスをDynamoDBで管理するようにし、状態が変更されたタイミングでメールを送る(異常発生/復旧)という仕組みにしてみました。
監視対象のサーバ情報はS3から取得してくるようにします。
まずはサーバ死活監視とメール送付部分の作成
状態の監視は後ほど加えるとして、まずはメインとなるサーバ死活監視とメール送付のLambda実装です。Python3.6で実装しています。
サーバ死活監視処理
S3から監視対象のデータを持ってくるところと、サーバ監視を実施してエラーが発生したサーバの一覧を作成する処理です。Lambda実行の際の権限の割り振り忘れに注意。
def get_target_servers():
s3 = boto3.resource('s3')
obj = s3.Object(BUCKET_NAME, OBJECT_NAME)
response = obj.get()
body = response['Body'].read()
return body.decode('utf-8')
def check_target_servers(target_json):
data = json.loads(target_json)
servers = data['servers']
error_servers = []
for server in servers:
name = server['name']
url = server['url']
try:
res = requests.get(url)
if res.status_code != 200:
error_servers.append(server)
except Exception:
error_servers.append(server)
if len(error_servers) == 0:
print("Successful finished servers checking")
else:
response = send_error(name, url, error_servers)
print("Error occured:")
print(response)
print(error_servers)
S3上には以下のようなjsonデータを配置するようにします。
{
"servers": [
{ "name": "googlea", "url": "http://www.google.coma" },
{ "name": "googleb", "url": "http://www.google.comb" },
{ "name": "google", "url": "http://www.google.com" }
]
}
上記を監視対象とすると、最後以外がアクセス失敗するので、エラーメールが飛ぶことになります。
以下の値は環境変数として定義しており、Lambda実行前に定義が必要です。
変数名 | 内容 |
---|---|
S3_BUCKET_NAME | S3の対象バケット名 |
S3_OBJECT_NAME | S3の対象オブジェクト名 |
SNS_TOPICS_NAME | SNSの送付対象トピック名 |
DDB_TABLE_NAME | DynamoDBのテーブル名 |
メール送付処理
とりあえずこんな感じで書けばSNSは呼び出せるので、これをカスタマイズします。
import json
import boto3
sns = boto3.client('sns')
def lambda_handler(event, context):
sns_message = "Test email"
topic = 'arn:aws:sns:us-east-1:<ACCOUNT_ID>:<TOPICS_NAME>'
subject = 'Test-email'
response = sns.publish(
TopicArn=topic,
Message=sns_message,
Subject=subject
)
return 'Success'
カスタマイズをしてこんな感じでメソッドにして組み込みました。
def send_error(name, url, error_servers):
sns = boto3.client('sns')
sns_message = "Error happens:\n\n" + json.dumps(error_servers, indent=4, separators=(',', ': '))
subject = '[ServerMonitor] Error happens'
response = sns.publish(
TopicArn=SNS_TOPICS_NAME,
Message=sns_message,
Subject=subject
)
return response
死活監視でエラーが発生すると以下のようなメールを受け取れます。内容は質素ですが、とりあえずこれで良しとします。
Error happens:
[
{
"name": "googlea",
"url": "http://www.google.coma"
},
{
"name": "googleb",
"url": "http://www.google.comb"
}
]
DynamoDBを使った状態管理
さて、このままでも監視はできますが、エラー発生と復旧がわかった方が良いので、DynamoDBを使って状態管理をし、変化があった場合にメールを送付するように改修します。
「url」をプライマリキー、「name」をソートキー(レンジキー)として「server-monitor」というテーブルを作成します。
取得と実装はこんな感じになります。これをカスタマイズして今回の処理に適用します。
import boto3
import json
from boto3.dynamodb.conditions import Key, Attr
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('server-monitor')
def lambda_handler(event, context):
#add_server()
#get_server()
return 'Finish operation'
def get_server():
items = table.get_item(
Key={
"url": "http://www.google.com",
"name": "google"
}
)
print(items['Item'])
def add_server():
table.put_item(
Item={
"url": "http://www.google.com",
"name": "google",
"status": True
}
)
カスタマイズして組み込んだ結果はこちらになります。
def check_status(url, name):
status_ok = True
try:
items = dynamodb.Table(DDB_TABLE_NAME).get_item(
Key={
"url": url,
"name": name
}
)
status_ok = items['Item']['status']
except:
status_ok = None
return status_ok
def add_server(url, name, status):
dynamodb.Table(DDB_TABLE_NAME).put_item(
Item={
"url": url,
"name": name,
"status": status
}
)
死活監視後のエラー判定部分も、DynamoDBから現在の状況を確認してメール通知をする処理の追加が必要になります。こんな感じです。
def check_target_servers(target_json):
data = json.loads(target_json)
servers = data['servers']
status_changed_servers = []
for server in servers:
name = server['name']
url = server['url']
status_ok = check_status(url, name)
try:
res = requests.get(url)
if res.status_code != 200:
if status_ok != False:
server['status'] = "Error"
status_changed_servers.append(server)
add_server(url, name, False)
else:
if status_ok == False:
server['status'] = "Recover"
status_changed_servers.append(server)
add_server(url, name, True)
except Exception:
if status_ok != False:
server['status'] = "Error"
status_changed_servers.append(server)
add_server(url, name, False)
if len(status_changed_servers) == 0:
print("Successful finished servers checking")
else:
response = send_error(name, url, status_changed_servers)
print("Error occured:")
print(response)
print(status_changed_servers)
動作確認
今までの実装でエラーが発生した時には「Error」と、復旧した時には「Recover」という内容のメールが送信されるようになっているはずです。動作確認をしてみます。
以前作成したWebページがS3上にあるので、アクセス権限を変更してテストしてみます。
Three.jsのかっこいいサンプルとAWSを連携させてみた
OKパターンで実行してみる。。。
メールは送信されず、DynamoDBにデータだけ追加されました。
さて、アクセス権限を変更して、アクセスできなくしてから再度実行してみます。無事エラーが発生してメールが飛んで来ました。状態は「Error」です。
Server Status Changed happens:
[
{
"name": "s3-test",
"url": "https://s3.amazonaws.com/xxxxxx/xxxx/css3d_periodictable.html",
"status": "Error"
}
]
DynamoDBのデータもエラーを表す「false」にstatusカラムが変更されています。
もう一度実行しても、エラーメールは飛んでこないようです。無事状態判定をしてくれています。DynamoDBの値も変化なしです。(キャプチャだけだと何もわかりませんがw)
それではページを復旧してみます。またアクセス権限を元に戻してアクセス可能にし、サーバ死活監視処理を実行してみます。
Server Status Changed happens:
[
{
"name": "s3-test",
"url": "https://s3.amazonaws.com/xxxxxx/xxxx/css3d_periodictable.html",
"status": "Recover"
}
]
無事復旧メールが届きました。これで完成です。
まとめ
サーバの死活監視を、状態管理しながら実施してみました。今回はAWSのサービスを中心に実現しましたが、おかげでトータル数時間レベルで実現できました。かかるコストも抑えつつ、実際に使えそうなものがこれくらいスピーディーに作れてしまうのは、さすがAWSのマネージドサービス、といったところです。
#[おまけ]ライブラリの管理
Lambdaでデプロイパッケージを作成する際、普通に実装すると外部ライブラリをプロジェクトのルートディレクトリに置くことになるので、あまり綺麗なパッケージ構成になりません。
そこで今回、こちらのページを参考にさせてもらいながら、別ディレクトリにパッケージを配置してく方法をとりました。
# pip install -U requests -t ./lib
で、Pythonにはこういう処理を追加する。
sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'lib'))
import requests
こうするだけでlib配下のパッケージを見に行ってくれるようになりました。これはディレクトリ管理することを考えるとかなり助かりました。実際serverlessコマンドでデプロイしても、正しく動きました。