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コマンドでデプロイしても、正しく動きました。



