AWS Lambda + Serverless FrameworkでSlackに定期的にメッセージを投稿する

f:id:nafuka11:20200318221150p:plain

AWS Lambda + Serverless Frameworkで、AtCoderの特定ユーザの成績(AC数、Rated Point Sum)を定期的にSlackに投稿する仕組みを作りました。

成果物URL

github.com

技術構成

f:id:nafuka11:20200318220556p:plain
構成図

Serverless Framework

Serverless Frameworkは、サーバーレスアプリケーションを構築できるオープンソースCLIツールです。

AWS, GCP, Azureなど様々なクラウドプラットフォームにデプロイ、テストもできます。YAML形式でアプリケーションの定義を記述します。

Serverless Dashboardというサービスを使うと監視もできるようですが、今回は使っていません。

個人的にサーバーレスだと嬉しい点は2点あります。

  1. 文字通りサーバがないところ。サーバのお守りをすることなく、ただ処理だけ記述すればいいのが嬉しい。
  2. 使った分だけ課金。今回は無料枠で十分収まる。

AWS

AWSとはAmazon Web Serviceの略で、Amazonが提供しているクラウドプラットフォームです。

AWS Lambda

AWS LambdaはAWSが提供しているサービスの1つです。サーバを管理することなくコードを実行できます。

ユーザの成績の取得、Slackの通知はLambda上で行っています。

Amazon CloudWatch Events

Amazon CloudWatch Eventsは他のAWSのサービスの変化や特定の時間をトリガーとして、他のサービスを実行できるサービスです。

今回はcron式を使って1日ごとにLambdaを実行するようにしています。

Amazon CloudWatch Logs

Amazon CloudWatch LogsでAWSのサービスからのログを一元管理することができます。

Lambdaで実行したログは自動でここに保存されます。

AWS Systems Manager Parameter Store

AWS Systems Manager Parameter Storeは、設定・機密管理の情報をkey-value形式で保管できるサービスです。

SlackのIncoming Webhook URLをうっかりGitHubにpushしないよう、Parameter StoreにURLを保管しています。

AWS CloudFormation

ここまでAWSのいろいろなサービスを説明しましたが、それらを一つにまとめて管理できるのがAWS CloudFormationです。JSONまたはYAMLファイルを使い、サービスをまとめてデプロイすることができます。

Serverless Frameworkはデプロイ時、内部的にCloudFormationを使ってデプロイしています。

テスト

pytest

pytestPython用のテストフレームワークです。

Python標準ライブラリのunittestと比べ、

  • テスト失敗時の表示が分かりやすい。

  • Pythonicにassertを記述できる(unittestはXUnitライクな書き方です)。

のが魅力だと思います。

pytest-mock

pytest-mockはpytestのプラグインです。unittest.mockのラッパーで、pytestのfixtureライクにmockを使うことができます。

pytestにあるmonkeypatchはただpatchするだけですが、unittest.mockは関数の呼び出し回数や渡された引数など取得でき、強力です。そのためプラグインを使うことにしました。

静的解析

mypy

mypyPythonスクリプトの型チェックを行うツールです。Pythonでは型ヒントを追加すると、IDEで静的解析され、自動補完などの恩恵を受けられます(型ヒントを追加しても、実行時に型チェックされるわけではありません)。

普段使っているIDE(PyCharm)でもある程度型チェックしてくれていたのですが、二重チェックで今回導入してみました。

flake8

flake8は、コードのエラー、循環的複雑度、コーディングスタイル(PEP8)をチェックするツールです。

これもPyCharmである程度チェックしてくれていたのですが、二重チェックで導入しました。

CI(GitHub Actions)

CIとは継続的インテグレーション(Continuous Integration)の略で、ソフトウェア開発手法の一つです。 リポジトリへのコミット毎に自動でテストを実行し、品質を常に保証する助けをします。

GitHub Actionsを使うと、GitHubのイベント(リポジトリへのpush、Pull Request作成など)をトリガーに任意のコマンドをGitHub上のコンテナで実行できます。

今回はリポジトリへpushされたら、mypy + flake8 + pytestを実行するようにしています。実行結果はSlatifyを使ってSlackに通知するようにしています。

f:id:nafuka11:20200318221448p:plain

ハマったところ

AtCoder Problems API

返ってくるデータはgzip圧縮されており、gzipを許可するヘッダーが必要です。許可していないとリクエストがブロックされるそうです。

Content-Encoding - HTTP | MDN

gzipを有効にしていないリクエストが多すぎて転送料が高すぎて死ぬ · Issue #211 · kenkoooo/AtCoderProblems

Serverless Framework

ホワイトリスト形式でデプロイするファイルを記述する方法が分からずにいましたが、以下を参考に設定することができました。

Package excludes do not seem to work - Serverless Framework - Serverless Forums

pytest

全般

最初、型ヒントを使っていなかったのですが、引数からフィクスチャを受け取るため、型ヒントをつけないと何を受け取るのか分からなくなるなと感じました。

tmpdir

tmpdirはテスト用に一時的なディレクトリを作成するフィクスチャです。

今はtmpdirでなく、 tmp_path 推奨のようです。tmpdirはpy.path.LocalPathを返しますが、tmp_pathは pathlib.Path を返し、扱いやすいです。

Temporary directories and files — pytest documentation

pytest-mock

テスト対象のモジュールでmock対象をimportしている場合、mockする時のモジュール名に気をつける必要があります。

https://docs.python.org/ja/3/library/unittest.mock.html#where-to-patch

mypy

  • サブディレクトリ内を走査するために__init__.py を作る必要があります。
  • --ignore-missing-importsで解決できないモジュールのimportを無視することができます。デフォルトで実行すると大量にimportエラーが出たため、このオプションをつけて対処しました。

作ってみて

定期的にSlackに投稿するだけなら、clasp + GASでやるのも良かったかもしれません。あちらの方がGoogleアカウントを持っていれば誰でもすぐデプロイできるし、スプレッドシートでデータを管理できて便利だし。

テストを書いたり、静的解析してみたり、それらを自動化してみたり、以前からやってみたかったことができてとても満足しました。

課題

  • AC数、Rated Point Sumの増分を表示したいです。
  • 現状だとuseridリストをファイルで持っており、変更するたびにデプロイする必要があります。slash commandを使ってSlack上で追加・削除できるようにしたいです。