こんにちは!SeeedK.K.の中井です。
みなさんお気づきでしょうか? 最近弊社のコーポレートサイトがたまに落ちていたことに。 社内の人間が気づいたり、SNSなどでご連絡いただいた場合には速やかに処置してはいるのですが、 サーバーが落ちている時間は思っているよりもずっと長かったのでしょう。
「コーポレートサイトにアクセスできない≒会社の信用問題」
になることもありますので、対策が必要ですね。 落ちる原因を特定するのが最善なのでしょうが、少し腰が重たいのも事実。 簡易的に外部からサイトにアクセスしてみて正常に稼働しているかどうかさえわかれば対処は可能です。 システム的には簡単なので、今回は自作することにしてみました。
プラットフォームはAzure Functionsで
機能としては周期的に対象のURLにアクセスしてみてサイトがダウンしているかどうかを確認し、異常状態のときにslackにメッセージをポストするといったものです。そのため、インターネットに接続可能であれば良いので、 Wio TerminalやRaspberry Piなどでも作成することができます。 ただ、インフラの信頼性などもあるので安定しているクラウド上で稼働させたいため、 今回は「Azure Functions」を使うことにしました。
- 対応するプログラミング言語: C#、Java、JavaScript、Python、PowerShell
どれもあまり触ってきたことがないのでPythonでやることに。(根拠は特にありません) また、定期的にWebサイトにアクセスを試みるのでTimer Triggerを利用します。
ローカル環境でプログラミング
Azure Functionsのチュートリアルを参照しながら作業を行いました。
requirements.txt
Webサイトにアクセス時にエラーだった場合は、slackに通知するように設定するため、 作成したプロジェクトのrequirements.txtに下記のように設定を追加。
azure-functions slackclient requests_oauthlib
scheduleパラメータ
周期実行のためのscheduleパラメータを与えます。scheduleパラメータはNCRONTABで与えます。crontabに似た書式ですが秒を扱えるように拡張されています。
このscheduleパラメータはfunction.jsonで指定するのですが、オンラインデバッグする時に変更したくなるパラメータのため、ドキュメントに説明があるようにアプリケーション設定に持たせることにしました。 function.jsonのscheduleパラメータに下記のように設定し、local.settings.jsonにパラメータを記述します。
- function.json
"schedule": "%ScheduleAppSetting%"
- local.settings.json
"ScheduleAppSetting": "*/30 * * * * *"
slack用のパラメータ
slackのアクセストークンやチャンネルIDはプログラム自体に直接記述するのはイマイチのため、アプリケーション設定(環境変数)に持たせることにしました。 ローカル上では、local.settings.jsonに記述しておきます。
- local.settings.json
"SLACK_TOKEN": "アクセストークンを記述", "SLACK_CHANNEL_ID": "チャンネルIDを記述"
監視対象のURL
監視対象のURLは、今のところ変更の予定はないのでベタ書きしています。 今後監視対象を増やしていくようになれば、Table Storageなどに移行したいと思います。
作ったコード
import os import json import datetime import logging import requests from urllib3.util import Retry from requests.adapters import HTTPAdapter from slack import WebClient from slack.errors import SlackApiError import azure.functions as func slack_token = os.getenv("SLACK_TOKEN") slack_channel_id = os.getenv("SLACK_CHANNEL_ID") client = WebClient(token=slack_token) targets = ['https://www.seeed.co.jp/'] in_errors = {} def send_notification(message): logging.info(' ==> postMessage: ' + message) for i in range(3): try: response = client.chat_postMessage(channel=slack_channel_id, text=message) except SlackApiError as e: logging.error('failed to chat_postMessage: %s', e.response['error']) else: break def main(mytimer: func.TimerRequest) -> None: jst_timestamp = datetime.datetime.now( datetime.timezone(datetime.timedelta(hours=9), name='JST')).isoformat() if mytimer.past_due: logging.info('The timer is past due!') logging.info('Python timer trigger function ran at %s', jst_timestamp) session = requests.Session() retries = Retry(total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504]) session.mount("http://", HTTPAdapter(max_retries=retries)) session.mount("https://", HTTPAdapter(max_retries=retries)) for url in targets: error_message = '' try: status_code = requests.get(url, timeout=(10.0, 30.0)).status_code except requests.exceptions.ReadTimeout: error_message = 'Timeout' except requests.exceptions.ConnectionError: error_message = 'Connection error' else: if status_code == 200: pass else: error_message = "Status Code " + str(status_code) in_error = in_errors.get(url, False) if not error_message: logging.info('[ OK]: %s', url) if in_error == True: in_errors[url] = False message = '- ' + url + ' is running normally.' send_notification(message) else: logging.info('[ERR]: %s (%s)', url, error_message) if in_error == False: in_errors[url] = True message = '- An error has been detected on ' + url + '.' send_notification(message)
深夜帯や休日なんかにサイトがダウンしていてslackにメッセージがバンバン飛んでくるのを防止するために、状態が変化した時のみ通知されるようにしました。
まとめ
今回作成したWebサイト監視アプリですが、5分に1回起動させるようにすると大体月100円未満のコストで運用することができます。サーバーレスでお手軽にアプリケーションを作成できるサービスに感謝ですね。
変更履歴
日付 | 変更者 | 変更内容 |
---|---|---|
2020/11/11 | mnakai | 作成 |