AWS Lambda + Serverless Frameworkで、AtCoderの特定ユーザの成績(AC数、Rated Point Sum)を定期的にSlackに投稿する仕組みを作りました。
成果物URL
技術構成
Serverless Framework
Serverless Frameworkは、サーバーレスアプリケーションを構築できるオープンソースのCLIツールです。
AWS, GCP, Azureなど様々なクラウドプラットフォームにデプロイ、テストもできます。YAML形式でアプリケーションの定義を記述します。
Serverless Dashboardというサービスを使うと監視もできるようですが、今回は使っていません。
個人的にサーバーレスだと嬉しい点は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
Python標準ライブラリのunittestと比べ、
テスト失敗時の表示が分かりやすい。
Pythonicにassertを記述できる(unittestはXUnitライクな書き方です)。
のが魅力だと思います。
pytest-mock
pytest-mockはpytestのプラグインです。unittest.mockのラッパーで、pytestのfixtureライクにmockを使うことができます。
pytestにあるmonkeypatchはただpatchするだけですが、unittest.mockは関数の呼び出し回数や渡された引数など取得でき、強力です。そのためプラグインを使うことにしました。
静的解析
mypy
mypyはPythonスクリプトの型チェックを行うツールです。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に通知するようにしています。
ハマったところ
AtCoder Problems API
返ってくるデータはgzip圧縮されており、gzipを許可するヘッダーが必要です。許可していないとリクエストがブロックされるそうです。
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上で追加・削除できるようにしたいです。