AWS Lambda でお手軽サーバーレスWeb定点観測 – 雨予報を通知する

AmazonLambda

決められた時間間隔で定期的に実行するバッチタイプの処理はサーバーレス化検討の筆頭です。今回はWebサイトを定点観測し、特定の要素が条件を満たした際にSlackへ通知する、ということを AWS Lambda でやってみたいと思います。以下のような構成です。

arc

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 をデプロイします。

lambda_deploy

CloudWatch Events

Lambda Function を起動するためのトリガーとして CloudWatch Events を使います。例えば1分ごとに起動するよう設定してみましょう。

cwe

これで実行準備が整いました。あとは、環境変数に具体的な値を入れることで、定点観測を行うことができます。

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! 天気の東京のページを見て、今日の天気が掲載されている要素を抽出し、「雨」が含まれていたら通知します。

yahoo

Slackの通知はこんな感じ。

slack

まとめ

CloudWatch Events と AWS Lambda を使って定点観測ツールを作ることができました。今回は天気予報でしたが、使い方次第で他にもたとえば欲しい商品の入荷情報や、予約の空き状況などを観測することも可能だと思います。

AWS Lambda のような Function as a Service 登場前までは、自分のマシンにせよ、クラウド環境にせよ、どうしても「アプリケーションを実行する場所」と「実行する人(フレームワーク)」を考えなくてはなりませんでした。AWS Lambda の登場によって、今回のように小さなCRONジョブであればサーバー構築を完全にスキップすることができるのでとても便利になったと感じます。Lambda Function は AWS の他のサービスと組み合わせれば、さらに高度な処理(HTTPリクエストやストリーミングなど)も実装できるでしょう。他にもどういったことができるか検討して、実際に試していきたいと思います。

参考

AWS Cloud Roadshow 2017 福岡