6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

サーバ死活監視→メール送付をLambda、AmazonSNSで実現する

Posted at

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にデータだけ追加されました。

スクリーンショット 2017-09-24 19.17.32.png

さて、アクセス権限を変更して、アクセスできなくしてから再度実行してみます。無事エラーが発生してメールが飛んで来ました。状態は「Error」です。

Server Status Changed happens:

[
    {
        "name": "s3-test",
        "url": "https://s3.amazonaws.com/xxxxxx/xxxx/css3d_periodictable.html",
        "status": "Error"
    }
]

DynamoDBのデータもエラーを表す「false」にstatusカラムが変更されています。

スクリーンショット 2017-09-24 19.21.23.png

もう一度実行しても、エラーメールは飛んでこないようです。無事状態判定をしてくれています。DynamoDBの値も変化なしです。(キャプチャだけだと何もわかりませんがw)

スクリーンショット 2017-09-24 19.23.43.png

それではページを復旧してみます。またアクセス権限を元に戻してアクセス可能にし、サーバ死活監視処理を実行してみます。

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

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?