AWS LambdaとBeautifulSoupを使ってPUBGが落ちていることをDiscordに通知する

# 序章

さて皆様、PUBGというFPSゲームはご存知でしょうか。
"PLAYERUNKNOWN'S BATTLEGROUNDS"の略で今年中頃から世界規模で流行しています。
Steamで同時アクセス数世界一になるほどの話題作なのでゲーマーでなくてもひょっとしたら耳にしたことがあるかもしれませんね。
その内容をザックリ1行で表現すると

* 人殺しが得意なフレンズ100人がやばんなちほーで繰り広げるズッキュバッキュン大騒ぎ

という感じです。
今回の記事には直接関係無いので詳細は割愛しますが、もし興味があればググッて下さい。
私は友人に誘われ面白そうだったので購入し最近は会社の同僚も巻き込んでちょくちょくプレイしています。

で、このゲームはまだアーリーアクセスでして、突如サーバーが落ちたりプレイに支障を来たすレベルのラグが発生したりするのは日常茶飯事です。
金曜日の夜に鼻息荒くゲームを起動したのに全然プレイできない、といった肩透かしは非常に辛いところでありせめて事前にわかっておきたく、またプレイ中に中断するハメになった際もイチイチTwitterや障害情報共有サイトをチェックするのは中々に面倒です。
そこでこれを自動化しチャットツール（後述）に通知されるよう実装しましたのでその内容をここに共有したいと思います。

# 非IT系用語

* Discord
    * 最近ゲーマー界隈で流行っているボイスチャットツール
    * 無料
    * マルチリージョン対応（自分で立地を選べ、更に変更も簡単）
    * Slackライクに色々と外部連携可能
    * TeamSpeak3, Ventrilo, Skypeといったボイスチャットツールが一気に陳腐化した
* Steam
    * 15年くらい前からある老舗オンラインゲームのダウンロード販売プラットフォーム
    * ダウンロード販売のデファクトスタンダード
* アーリーアクセス（早期アクセス）
    * まだ開発中のゲーム等を購入する仕組み
    * 開発側は繋ぎ資金を得たり早期にユーザーからのフィードバックを得られる
    * プレイヤー側は興味のあるゲームをいち早く体験したり早期購入限定の特典を得たりできる
    * 但し品質や開発完了の保証は無く途中でプロジェクトがご破算になったりする場合もあり、投資的な要素がある

# 基本的な仕組み

安直に考えれば死活監視してステータスを自動で通知してやればいいんじゃないかと思いますが公式のAPI等は（当然）提供されていません。
代替策として、下記の障害と思しき事象発生を共有するサイトがありましたのでそちらからデータを拝借し閾値以上の報告が発生していたらDiscordに通知してやることにしました。

http://outage.report/playerunknowns-battlegrounds

このサイトは（中身を見て貰えばすぐにわかると思いますが）「俺PUBGプレイできねぇ、多分これ障害だぜ」と思ったらボタンをポチッと押して短時間に多数のレポートがあればまず間違いなく障害起こっているよね、という具合に判断することができるものです。
（レポート元の地域を判定し地図上でリアルタイムにポップされるというちょっと気の利いた作りで好き）

ちなみにPUBGだけでなくメジャーなサービス（GoogleDriveとかUberとか）向けにも色々あります。

# 詳細な仕様

* 障害情報は先に挙げたサイトをBeautifulSoupでスクレイプし取得する
* 障害レポート数が所定の閾値以上であった場合にDiscordへ通知する
    * 誤解か何かで常時2～5件程度はレポートが上がっているので10件を通知の閾値とする
* Discordへの通知は簡便なWebhookを使用する
* AWS Lambda(Python3)でファンクションを作成し、それをCloudWatch Eventsでキックする
    * IaaSでBOTサーバーを立ててcron、というのは流石にダサいし高いのでナシ
* 何らかの理由でレポート数の取得に失敗したりした場合はCloudWatchにログを出力する

# 実際に作る

## 前提条件

* 開発環境
    * Python3.6系がインストール済みであること
    * pip, virtualenvの基本的な使い方をわかっていること
* AWS
    * AWSのAPIキーが開発機に設定済みであること
    * Lambda用のIAMロールを作成済みでARNがわかること
    * LambdaからCloudWatch Eventsの設定ができること
* Discord
    * Webhook URLを発行済みであること

## virtualenvで隔離環境を作る

```bash
ca@localhost:~$ virtualenv --python=`which python3.6` discord
ca@localhost:~$ source discord/bin/activate
(discord) ca@localhost:~$ python -V
Python 3.6.1
```

## pipでライブラリーとLambdaデプロイツールをインストールする

```bash
(discord) ca@localhost:~$ pip install beautifulsoup4, requests
(discord) ca@localhost:~$ pip install lambda-uploader
(discord) ca@localhost:~$ lambda-uploader --version
1.2.0
```

## 作業用ディレクトリを作成しそこに移動する

```bash
(discord) ca@localhost:~$ mkdir lambda_pubg
(discord) ca@localhost:~$ cd lambda_pubg
(discord) ca@localhost:~/lambda_pubg$ 
```

## Lambda用のファイル4つを作成する

* lambda.json
    * Lambdaの設定ファイル
* event.json
    * 今回は使用しないので空ファイルでOK
    * ファイル自体が無いとエラーになるので適当に作る
* requirements.txt
    * Pythonの外部ライブラリー一覧
* pubg_problem_reporting.py
    * Lambdaで実行されるコード本体


## 各ファイルの内容

```json:lambda.json
{
	"name": "pubg_status_reporting",
	"description": "description（何でもいい）",
	"region": "ap-northeast-1",
	"handler": "pubg_reporting.lambda_handler",
	"role": "arn:aws:iam::00000000:role/lambda_pubg_reporting_role",
	"timeout": 20,
	"memory": 128
}
```

```json:event.json
空でおk
```

注：私の環境では↓のようになったが、
`pip freeze > requirements.txt`で作成すること

```text:requirements.txt
beautifulsoup4==4.6.0
boto3==1.5.1
botocore==1.8.15
certifi==2017.11.5
chardet==3.0.4
docutils==0.14
idna==2.6
jmespath==0.9.3
lambda-uploader==1.2.0
python-dateutil==2.6.1
requests==2.18.4
s3transfer==0.1.12
six==1.11.0
urllib3==1.22
virtualenv==15.1.0
```

```python3:pubg_problem_reporting.py
# coding: utf-8
import json
import logging
import requests
import urllib.request
from bs4 import BeautifulSoup


ERROR_REPORT_THRETHOLD = 10 # Discordへ通知するエラーレポート数の閾値
DISCORD_WEBHOOK_URL = "https://discordapp.com/api/webhooks/0000000000/xxxxxxxxxxxxxxxxxxxxxxxx"
STATUS_SOURCE_URL = "http://outage.report/playerunknowns-battlegrounds"
STATUS_SOURCE_REQUEST_HEADERS = {
	"User-Agent": "HogeFugaPiyo",
}

logger = logging.getLogger()
logger.setLevel(logging.INFO)


# Discordにメッセージを送信
def send_discord(content):
	payload = json.dumps({
		"content": content,
	})
	try:
		webhook_resp = requests.post(
			DISCORD_WEBHOOK_URL,
			data=payload,
			headers={"Content-Type": "application/json"},
		)
	except:
		logger.error("Error: failed to post Discord")
		return 500
	return webhook_resp.status_code


# リクエストを作成しフェッチする
def fetch():
	req = urllib.request.Request(
		url=STATUS_SOURCE_URL,
		headers=STATUS_SOURCE_REQUEST_HEADERS,
	)
	try:
		get_resp = urllib.request.urlopen(req)
	except:
		message = "Error: failed to urlopen"
		logger.error(message)
		return send_discord(message)
	return get_resp


# BeautifulSoupにレスポンスを渡しレポート数を取り出す
def get_count_problem_report(resp):
	try:
		soup = BeautifulSoup(
			resp.read(),
			"html.parser",
		)
	except:
		message = "Error: failed to make BeautifulSoup"
		logger.error(message)
		return send_discord(message)
	
	try:
		problem_report_count = int(soup.find("text").string)
	except:
		message = "Error: failed to get problem report count"
		logger.error(message)
		return send_discord(message)
	
	return problem_report_count


def lambda_handler(event, context):
	resp = fetch()
	problem_report_count = get_count_problem_report(resp)
	
	# 閾値を超過していたらDiscordにレポートする
	logging.info("Reported problems count %d" % problem_report_count)
	if problem_report_count >= ERROR_REPORT_THRETHOLD: # エラーレポートが閾値以上発生している場合は通知する
		return send_discord("Many problems are reported now: " + str(problem_report_count) + "\n" + STATUS_SOURCE_URL)
	
	return 204

```

## BeautifulSoup用の細工（重要）

BeautifulSoupはC言語のライブラリを使用しているようで、予め下記のコマンドを叩き別途場所指定でインストールしておかなければならない。
これを怠ると `invalid syntax (__init__.py, line 53)` と言われLambdaが起動しない。
スタックトレースも出ないので特定にえらい苦労した。
他のライブラリーは `lambda-uploader` に任せてOK

```bash
pip install beautifulsoup4==4.6.0 -t .
```

[参考リンク](https://runble1.com/aws-lambda-python3-mac/)

## コードをLambdaにアップする

```bash
(discord) ca@localhost:~/lambda_pubg$ lambda-uploader --profile myaccount
Î» Building Package
Î» Uploading Package
Î» Fin
(discord) ca@localhost:~/lambda_pubg$ 
```


## Lambdaのテスト

コンソールを開きテストを実行します。
渡すデータは何でもOK（使わない）。
設定済みのものが無ければHello, World辺りをベースに適当に作成してOKです。
「テスト」ボタンを押下してエラーが発生しないかチェックしましょう。

![1.png](https://qiita-image-store.s3.amazonaws.com/0/134838/3f950ff2-8357-09af-96f9-b63e10948b41.png)


通知された場合にどんな具合になるかテストしたければ一時的にif分に細工してデプロイするのが簡単です

```diff
- if problem_report_count >= ERROR_REPORT_THRETHOLD:
+ if problem_report_count >= 0:
```

![3.png](https://qiita-image-store.s3.amazonaws.com/0/134838/49ff02ea-705a-c000-9417-a89f4a5f97ab.png)



## CloudWatch Eventsの設定

特段難しいことは無いと思うので、画面の流れに沿って設定します。
インターバルは常識の範囲内で。
**先方に迷惑をかけてはいけません！**

![2.png](https://qiita-image-store.s3.amazonaws.com/0/134838/81fb3708-6dcc-9a6f-368e-ddc667643801.png)

# 諸注意

## Ubuntuでパッケージのエラーが発生する

Ubuntu16.04にはバグがあり、pipにおかしなメタデータを渡しているようで `pip freeze` した際に `pkg-resources==0.0.0` が含まれてしまいます。
これが起こる場合は単純に `requirements.txt` の中から当該行を削除してしまって大丈夫です。

[参考資料](https://github.com/pypa/pip/issues/4022)

ちなみに具体的な環境の例としてはWSLのUbuntuでこの事象が発生します。
（Beta時代に作った私の環境です。いま新規に作ると解消されてるかも）

# 終わりに

まだこれを実装して数時間しか経っていないので、実際に問題発生した時の使い心地については不明です。
もう数日もすれば効果を体感できるでしょう。
何か問題やアップデートがあれば随時ここに追記したいと思います。

AWS Lambdaについてはつい最近Python3に対応しており今回のきっかけで初めて触りましたが特段問題無く使う事ができました（先に書いたC言語ライブラリ云々の問題はともかく……）。
FaaSをコアにした所謂サーバーレスアーキテクチャーが今後どのような発展を遂げるのか私には今一つ予想が付きませんが、少なくともこういった小規模なBOTやバッチ処理的な使い方では手間要らず金要らずでかなり有用であると感じていますので今後も動向はチェックしていきます。

それでは皆様、良いPUBGライフを。