AWS Lambda でお手軽サーバーレスWeb定点観測 – 雨予報を通知する
決められた時間間隔で定期的に実行するバッチタイプの処理はサーバーレス化検討の筆頭です。今回はWebサイトを定点観測し、特定の要素が条件を満たした際にSlackへ通知する、ということを AWS Lambda でやってみたいと思います。以下のような構成です。
Lambda Function
scala-scraperを使って実装しました。シンプルなDSLでスクレイピング処理を書けることが特徴です。このあたりはScala製のライブラリを使うメリットのひとつですね。プログラムの構造ですが、まず、Lambda Function の環境変数として次の表のものを用意します。
環境変数名 | 用途 |
---|---|
TARGET_URL | HTML取得先 |
TARGET_CSS | 判定対象にする要素をCSSセレクタで指定します |
CHECK_STRING | 判定対象の文字列を指定します |
CHECK_TYPE | 判定方法を指定します。equal, not_equal, contains, not_contains を指定可能 |
WEBHOOK_URLS | 通知する先のURLです(Slack想定) |
NOTIFY_BODY | 通知内容です(Slack想定) |
TIMES | 一度の実行でポーリングをトライする回数 |
次にこれらの環境変数を使って要素の判定および通知を行います。実際のコードを見てみましょう。
HTMLを取得し、判定対象をチェックする
package lambda import net.ruippeixotog.scalascraper.browser.JsoupBrowser import net.ruippeixotog.scalascraper.dsl.DSL._ import net.ruippeixotog.scalascraper.dsl.DSL.Extract._ import net.ruippeixotog.scalascraper.dsl.DSL.Parse._ import net.ruippeixotog.scalascraper.model._ class ScrapeClient { val url = sys.env("TARGET_URL") val checkString = sys.env("CHECK_STRING") val checkType = sys.env("CHECK_TYPE") val css = sys.env("TARGET_CSS") lazy val siteText = getText private def getText: String = { val browser = JsoupBrowser() val doc = browser.get(url) doc >> text(css) } def toNotify: Boolean = { val s = siteText val c = checkString checkType match { case "equal" => s == c case "not_equal" => s != c case "contains" => s.contains(c) case "not_contains" => !s.contains(c) case _ => s == c } } }
通知する
import play.api.libs.json.Json import scalaj.http.Http class Notify { val webhooks = sys.env("WEBHOOK_URLS") val notifyBody = sys.env("NOTIFY_BODY") def notifySlack: Unit = webhooks.split(",").foreach(send) private def send(url: String): Unit = { val req = Http(url).postData("payload=" + reqBody) val res = req.asString println("[request] : " + req) println("[response] : " + res) } private def reqBody: String = Json.parse(s""" |{ | "username": "observer", | "text": "$notifyBody", | "icon_emoji": ":ghost:", | "link_names": 1 |} """.stripMargin).toString }
Lambda Function 用のエントリポイントを定義する
import com.amazonaws.services.lambda.runtime.{ Context, RequestHandler } trait NotifyComponent extends RequestHandler[java.lang.Object, String] { val client: ScrapeClient val notifySlack: Notify val times = sys.env.getOrElse("TIMES", "1").toInt override def handleRequest(input: Object, context: Context): String = { for (_ <- 0 until times) { val result = doTask println(result) Thread.sleep(10000) } s"Times: $times" } def doTask: String = { if (client.toNotify) { notifySlack.notifySlack } println("siteText: " + client.siteText) client.siteText } } class NotifyController extends NotifyComponent { override val client: ScrapeClient = new ScrapeClient() override val notifySlack: Notify = new Notify() }
デプロイする
まず、S3 に JAR をアップロードします。
$ git clone git@github.com:cm-wada-yusuke/lambda.git $ cd polling/functions/observer/ $ sbt assembly $ aws s3 cp target/apex.jar s3://wada/lambda/apex2.jar
次に、S3 から Lambda Function をデプロイします。
CloudWatch Events
Lambda Function を起動するためのトリガーとして CloudWatch Events を使います。例えば1分ごとに起動するよう設定してみましょう。
これで実行準備が整いました。あとは、環境変数に具体的な値を入れることで、定点観測を行うことができます。
Lambda Function の利用例 - 雨模様を通知する
実際に使ってみます。環境変数を以下のようにしました。
環境変数名 | 用途 |
---|---|
TARGET_URL | https://weather.yahoo.co.jp/weather/jp/13/4410.html |
TARGET_CSS | #main .forecastCity p.pict |
CHECK_STRING | 雨 |
CHECK_TYPE | contains |
WEBHOOK_URLS | https://hooks.slack.com/services/XXXX/XXXXX |
NOTIFY_BODY | @channel 今日は雨模様です。<https://weather.yahoo.co.jp/weather/jp/13/4410.html|Yahoo天気> |
TIMES | 1 |
これは何かというと… Yahoo! 天気の東京のページを見て、今日の天気が掲載されている要素を抽出し、「雨」が含まれていたら通知します。
Slackの通知はこんな感じ。
まとめ
CloudWatch Events と AWS Lambda を使って定点観測ツールを作ることができました。今回は天気予報でしたが、使い方次第で他にもたとえば欲しい商品の入荷情報や、予約の空き状況などを観測することも可能だと思います。
AWS Lambda のような Function as a Service 登場前までは、自分のマシンにせよ、クラウド環境にせよ、どうしても「アプリケーションを実行する場所」と「実行する人(フレームワーク)」を考えなくてはなりませんでした。AWS Lambda の登場によって、今回のように小さなCRONジョブであればサーバー構築を完全にスキップすることができるのでとても便利になったと感じます。Lambda Function は AWS の他のサービスと組み合わせれば、さらに高度な処理(HTTPリクエストやストリーミングなど)も実装できるでしょう。他にもどういったことができるか検討して、実際に試していきたいと思います。