AtCoder の過去問埋めを超楽にするサービスを作った話(ダイジェスト)

はじめに

こんにちは、CCS OB の @kakira9618 です。この記事は CCS Advent Calendar 2022 の20日目の記事です。

adventar.org

19日目(前日)はあきひよさんの 去年の自分からのバトンを受け取って半ば投げ捨てた人の記事 です。

表題の通り、Webサービスを作ったので記事を書きます……が時間があまりない(記事執筆開始: 2022/12/20 22:00)ので、ダイジェスト版(概説・動機・システム構成説明・実装で辛かったところ・評価・感想)でお送りします。色々考えながら作ったので、記事にできそうなことは他にもあるのですが、時間の制約上、個々の観点についてはまた今度……

前提知識

日本の競技プログラミングの最大手サイトです。プログラムを提出すると、予め用意されている複数のテストが実行されて自動採点されます。

何を作ったの?

AtCoder Companions というサイトを作りました (2022/12/11 リリース)。

告知ツイート

詳細はツイートの動画やサイトのAboutを確認していただくとして、ざっと概要の概要を説明すると、「同じ間違え方をした後に正解している他の提出を見つけて、間違いと正解の提出のコードの差分を表示する」サービスです。ざっと動画見てイメージしてもらうのが早そうです。

期間など

開発着手からリリースまで約2ヶ月でしたが、アイデア自体は1年ほど前からあったと思います。

動機(対外向け)

みんな AtCoder やってますか?僕は最近はできてないですね((

現役の人も、全くやってない人も、昔やってた人もいると思います。

AtCoder の腕前をあげる上で欠かせないのが、過去問演習です。

とりあえず、問題を解いて・・・サンプルケースもあってる。よし、提出!とするまでは良いのですが、かなりコレになりがちです(個人の感想です)

よくあるやつ

解説はAtCoderで閲覧できるので、方針が合っているかどうかというのは大体想像がつきます。サンプルテストケースが通ってない場合も、その入力をデバッグすることができるので問題ないと思います。問題は、方針もサンプルテストケースの出力も合っているはずなのに、一部テストケースだけなぜか通らない、というパターンです。

これがサッと解決すればよいのですが、大抵の場合はそうはいかず、解決するのに無限に時間がかかったり、人に聞いたり、あるいは諦める必要があったりしました(個人の感想です)。無限に考えてようやくAC(正解)しても、直した内容は単なるポカミスとかだったりして、これだけのことに自分はn時間も消費してしまったのか… となることがあると思います。 そしてそのような体験が続くと、AtCoderに飽きてしまいやらなくなる… みたいなことも十分有り得ると思います。

これをなんとかできないか、と考えてできたサービスが AtCoder Companions です。

AtCoder Companions に、そのような提出のURLを投げると、

  1. 同じ間違え方をしている提出を検索し
  2. その中から、最終的にAC(正解)にたどり着いている提出を抽出し
  3. 1.と2.のコードの差分を表示

してくれます。

AtCoder Companions のメイン画面

"同じ間違え方をしている提出" というのは、画像の Result Vector、つまり各テストケースにおける結果の配列が同じであるような提出を指しています。

同じ間違え方をしている提出がどのように正解にたどり着いているかがわかれば、必然的に自分の提出で修正しなければならない箇所がわかるわけですね。

裏・動機

業務(Web系) とかではシステムの大部分が出来上がっててその拡張を作るなどの機会が多いのですが、一からサービスを作ることは少ない(部署による)ので、そのような経験に欠けがちでした。

また、コストの許容ラインは個人サービスのほうがより低く設定されていることが多く、その辺一度作ってどんな感じなのか体験したいという裏の動機がありました。

真・動機

いえ、もっと突っ込むと、

  • 焦燥感:AtCoder Replay (前作)などを作ってから時間(3年)が経っており、そろそろなんか作らないとやばい。特にフロントエンドとか。
  • 好奇心:今持っている知識を使ってある程度まともなWebサービスを作ってみたらどの様になるのか、腕試ししてみたい。

という気持ちがありました。これが真の動機(?)です。

実装

さて、いきなりシステムの話になって申し訳ないです…🙇‍♂️

AtCoder Companions のインフラ全体像はこんな感じになりました。AWS様様です。

AtCoder Companions のインフラ構成図

全体的にサーバーレスな構成となっています。また、環境ごと独立してインフラを管理できるように、AWS SDKによってインフラはコード化されています。

基本的に、Static File のリクエストと API Call のリクエストで通る経路が違い、Static File は CloudFront を通って配信され、API request は APIGateway 経由で lambda が叩かれます。lambdaはDynamo DB にアクセスしたりします(Token用)

AWSとは別でクロール用のサーバーが動いており(これは今は手元PC)、AtCoder にクロールをして、その情報を Resource Bucket に送ります。この情報を CloudFront から署名付きで(理由は後述)配信することで、同じ間違え方をしている情報の取得が行えるようになっています。

また、フロントエンドは Vite + React + TypeScript + Tailwind + DaisyUI で、バックエンド(Lambda)は Node.js で、インフラ(AWS SDK)は TypeScript で書いてます。ほとんど JavaScript 系で書かれますね。

実装でやばかったところ

最終的には先程のような構成のサービスとなりましたが、当初はもっとシンプルに実装できる予定でした。 しかし、開発を進めていくとやらなければならないことがたくさん増えて増えて当初の予定よりだいぶ大きなプロジェクトになってしまいました。

最初はクローラーだけが後ろで動いている静的なサイト想定で作り始めたのですが……

  • CORSの関係で動的な仕組みを入れなきゃいけない、やばい
  • クローラーのアクセス数が多すぎて、やばい
  • クローラーは一歩間違えれば攻撃とみなされるのでポリシーを設定しなきゃ、やばい
  • DoS攻撃されるとやばいので制限入れなきゃ(お財布もだが、AtCoderを巻き込むので)、やばい
  • 制限入れるのであればチートができないようにしなきゃ、やばい
  • Google Analytics 便利だけど GDPR関連、やばい
  • 謎の課金、やばい
  • CI/CD周り・テストがない、やばい
  • ドキュメント書かなきゃ、やばい

などなど、やばいづくしで当初の想定の8倍1くらいの時間がかかりました😇

もうちょっと見積もりは丁寧にやりたいですね。とは言いつつ、やってみないと問題点が上がってこない事が多いので、悩みどころです。

大体の問題は解決したけど、まだいくつか解決していないところ(課金系の問題とかテストCI・CD周りとかはまだです)もありますので、今後の課題とします。

以下、詳細です。技術的な内容も含まれるので、興味無いかたは評判の項まで進めちゃってください。

CORSの関係で動的な仕組みを入れなきゃいけない、やばい

AtCoder Companions の特性上、AtCoderの個々の提出を一つ一つクロールする必要があります2

しかし、個々の提出ページはAPIとして他のサイトから利用されることを想定しておらず、AtCoder Companions 上の javascript からそのページの内容を取得し利用することはできないようになっています。 これでは、ソースコードの差分を表示するときに困ってしまいますので、自前でプロキシサーバーを立ててそこにリクエストを送る形としました。(自前サーバーであれば、CORS用のレスポンスヘッダーを挿入できるのでリクエストを間接的に行うことができるようになります)

しかし、これで静的な仕組みでつくれるだろう、という当初の目論見はあっけなく崩れ去ったのでした。

クローラーのアクセス数が多すぎて、やばい

ABC (AtCoder Beginner Contest)の場合、1つのコンテストあたり大体6万〜10万くらいの提出があります。まるで競技プログラミングサイトの制約みたいですね(?) その一つ一つの提出にアクセスするということは、つまり6万〜10万回以上のアクセスが発生するということです。

もちろんそれ用のプログラム(クローラー)を書いてスクレイピングするのですが、このオーダーの回数になってくると、しっかりとwaitをもたせてアクセスしないとDoS攻撃になってしまいます。 そして、しっかりと wait をもたせてクロールさせると、1コンテストあたり半日から1日くらいの時間がかかってしまうことになります。(EDPCなど提出数の多いコンテストの場合、更に5倍以上の時間がかかる) AtCoderには公開されているアルゴリズムのコンテストが1000弱くらいあるので、1周クロールが終わるのには年単位の時間がかかる計算になります。

公式側の対応として、提出一覧ページで各テストケースの情報が取れたり、APIを公開していただければアクセス回数は減ると思うのですが、普段は要らない情報だったりするので難しいところです。 AtCoder社長の chokudai さんとやり取りをしつつ、その辺りの仕組みの調整を行っている段階です。

クローラーは一歩間違えれば攻撃とみなされるのでポリシーを設定しなきゃ、やばい

前項で上げたとおり、かなりの回数のアクセスを行っているので、一歩間違えればDoS攻撃になってしまいます。 また、ソースコードなど著作性の認められるものの扱いは、慎重に行う必要があります3

この辺りで問題が起きるのを未然に防止するために、Crawling Policy という専用ページを作って、クロールする内容、アクセス頻度、利用目的を明記しました。 法的な文章は書くのはもちろん、読むのすら慣れていないので、このページを作るのには時間がかかりました。 また、全体としてUIに英語を使用しているので、英語で書いたほうが良いんじゃないか、という問題もありました4

この辺、良いまとめとかリソースがあればよいのですが、、、なかなかないですね。あったらぜひ教えて下さい。

DoS攻撃されるとやばいので制限入れなきゃ(お財布もだが、AtCoderを巻き込むので)、やばい

CORS対応したことで、自前のプロキシサーバーからAtCoderにアクセスする仕組みができたのは良いのですが、そうすると今度は「プロキシサーバーを踏み台にしてAtCoderにアクセスさせる」という攻撃ができるようになってしまいます。 踏み台にされた場合、AtCoderからみたアクセス元のIPはプロキシサーバーのアドレスです。これがAtCoderにBANされてしまうと、AtCoder Companions 全体に影響が出てしまいます(DoS攻撃の成立)

このリクエストはクローラーと違い、ユーザのブラウザから行うので、何らかの制限を入れる必要があります。 そこで、今回は新規ブラウザに対して Token を発行し、各APIを呼ぶには Token が必要になる、という仕組みを導入しました。 サーバー側では、その Token に紐づく API の使用回数をカウント・保存(Dynamo DB)していて、これによって、1日あたり○回までしか API を使えない、という制限を行えるようになりました。

具体的には「Find Companions API(同じ間違え方をした提出のメタ情報CSVのURLを取得するAPI)」と「Fetch Submission API(プロキシサーバーを通して提出のソースコードを取得するAPI)」の2つを制限の対象としています。

SQL系のDBとは違い、Dynamo DB はあまり触ってなかったので、慣れるのに時間を要しました。コストの算出方法なども独特で、auto scaling の設定などを考えて実装する必要がありました。

制限入れるのであればチートができないようにしなきゃ、やばい

もともと、Lambda が返せる情報量は5MBくらいなので、データはS3に予め上げておいて、そのURLを返すという方式にしています。

S3でのパスは、各テストケースに対する結果(Result Vector)のハッシュによって定まるようになっており、これによってある Result Vector に対応する提出の一覧を一発で取ってこれるようなデータ構造になっています。

前項の通り、Tokenによる制限を入れたのは良いのですが、そうすると今度はAPIを呼ぶことに価値ができてしまうので、その価値がなくなってしまうような脆弱性があってはいけません。 しかし、仕組み上 Find Companions API は、Result Vector がわかっていれば、手元でハッシュなどの計算を行うことによって直接URLの推測を行うことができてしまいます。 そのようにアクセスできてしまうと、簡単に制限の迂回ができてしまうことになります。

よってこの部分には Lambda によってファイルのURLに署名をつけて、そのURLを返すことにしています。 この署名が正しいかどうかをサーバーで検証してから、アクセスを許可することで、確実にAPIをコールした事がわかるし、静的ファイルに有効期限などの機能もつけられます。

現在はこの仕組みで動いていますが、ほとんどのリクエストは 5MB 以下なので Lambda で動的生成できるようにしたほうが無駄5が少なくなりそうで、現在実装検討中です。

Google Analytics 便利だけど GDPR関連、やばい

アクセス解析には Google Analytics を使っています。いろいろな情報が取れて基本的に便利なのですが、落とし穴もあります。それは、GDPR関連です。

詳細はググっていただくとして、日本国内だけではなく、GDPRの対象国へのサービスも行っていると認められる場合、GDPR対象国からのアクセスがあったときに、そのアクセスに紐づくCookieやIPなどの個人情報は GDPR が定める扱いをしなければなりません. (しないと訴訟されて多額の賠償金を払わなければならない、ということになっていて、実際にGoogle Fonts を使っていたら訴訟されて有罪になった例もあります)

個人か法人かには関係なく訴訟されたらまずいため、今回は 安全側に倒しGDPR 対象国からのアクセスをアクセス禁止にするという対応を取らせていただいています(対象国の皆様、申し訳ありません)

本来なら、きちんとポリシーを定義して、個人情報の取扱をしっかりとするのが望まれているはずですが、開発期間の関係(年内に完成させたかった)もあり、このような対応になってしまいました。今後の課題ですね。

謎の課金、やばい

AtCoder Companions は AWS の上に構築されているので、サーバー利用料はAWSに支払っています。が、無料枠というものがあり、今回の構成ではほとんど料金が発生しない… そのつもりでした。

しかし、蓋をあけてみると、やはり何かしら想定外の課金が起きていました6

この辺りの情報は、AWSコンソールの Billing Console の Free Tier から見れますので基本的にそれを見ながら調整すれば良いです。

想定外だったのは、

  • S3に対して細かいファイルをあまりにもたくさんアップロードしすぎたために、PUTにかかる料金が嵩んでいた
  • 謎のアラームが生えていて、無料分(10個まで)を超過していたこと

ですね。前者はすべてのレスポンスを S3 から返すのではなく、5MBを超えるものについてだけ動的にファイルを作って返すようにすれば解消しそうです(未実装)。

後者は、色々原因を調べていくと、どうやら Dynamo DB の auto scaling 用に設定されたアラームがカウントされてしまっているようでした。 今回はAWS CDKでインフラ環境を構築しており、環境(dev, live)ごとにTableをつくっていたので、auto scaling の設定なども環境が増えると倍増するようになっていました。 最終的には開発環境(dev)のauto scaling は要らないな、ということになり、そちら側の設定のみを削除することで対応しました。

CI/CD周り・テストがない、やばい

リリースまでには間に合いませんでした・・・😇 今後実装予定です。

とりあえず完成を急いだため、テスト・CI/CD周りは実装を後回しにしていました。しかし、その代償としてソースコードの Testablity が落ちてしまっています。 これからリファクタリングを行い、 Testablity を上げていかなければなりません。

デプロイ系のスクリプト化はちまちま作ってたので、テストなしのデプロイするだけのパイプラインを作ってもよいのですが、やはりテストがないとデグレとか色々心配になってしまうのでおとなしくテストしましょう。

ドキュメント書かなきゃ、やばい

まず、ソースコードに対するドキュメントがまだ配備されてないので、今後の課題です😇

また、一般利用者が読むドキュメントページ もあります。 このページは実装としては markdown で書いたものを Fetch して React Dom に変換して差し込むということをしているだけなのでシンプルなのですが、 ユーザの目線でドキュメントを書く、ということに慣れていないのでとても時間がかかりました。

内容面では、どのような内容を書いたら良いのかがわからなかったので、いろんなサービスの About っぽいページを参考にしながら書く内容を吟味しました。

デザイン面では、初期版では中央揃えと箇条書きが混ざって非常に読みにくくなっていたり、箇条書きのインデントがどんどん深くなって読みにくくなっていたり、画像の大きさが端末によっては適切でなくなってしまうという問題があり、修正しました。

アクセシビリティも重視しており、視覚に関する一部の情報がなくてもブラウザの支援機能等を利用してサイトの利用ができるように考慮しました7

iPhoneのリーダー機能、存在は知っていましたが、まともに使ったのはこれが初めてでした。

その他の観点

こちらのリプツリーに(項目だけ)あります。 実装以外にも、βテストしたりローカライズちょっとだけしてみたり、プロジェクト管理頑張ってみたり、色々してました。

評判・評価

というわけで、リリースには時間がかかってしまいましたが、嬉しいことにリリース後、かなりたくさんのポジティブなリアクションを頂きました。

AtCoder 社長(chokudai さん)にもポジティブな評価を頂けたようです。やったね。

動画再生数的には、UUが1.1万なので、AtCoderのアクティブなコンテスト参加者数(〜1万)を考えると、かなりの割合の方に見てもらえたんじゃないかと思います。

AtCoder Companions は新規利用の際にトークンが発行されるのですが、その数によると770人超の方に実際に使って頂けたようです。ありがとうございます。

Dynamo DB のItemの数

感想

とりあえず、目標としてたサービスを作りきることができ、ホッとしました。 更にリリースしたサービスがしっかり使われていて、かつポジティブな反応が数多くあったため、正直めっちゃ嬉しかったです。

またいつもめんどくさがってやらないことにも手を着けられたので、学びが非常に多かったです。 特に、(技術面、デザイン面、企画面、プロマネ面の学びももちろんありますが)法律面の学びがあったのは大きいと思います。

課題も解決できたし、満足感も得られたし、自分自身の学びにもなったので当初の目的(動機)はほぼ達成できたのではないでしょうか。

ただ、今後の課題ですね。としたところもたくさんあるので、まだゴールではなく、今後も開発を続けていきたいと思っています。

また、記事をアップロードするのが遅くなり申し訳ありません……🙇‍♂️ そしてダイジェストのはずだったのに結局1万文字以上書いてしまった……

まとめ

Web開発やばいけど楽しいのでみんなやろう。あと AtCoder Companions 使ってね。

明日は、@ichi-raven さんの 主にC++の話(?)です…… ってもう出来てる!?素晴らしすぎる(n時間遅刻した自分を見ながら)


  1. 当初は1週間で作り上げる予定でした。そんなうまくいくはずがないですね。
  2. 同じ間違え方をした提出をリアルタイムで検索するには、予めコンテストの提出一覧にアクセスし、各テストケースの結果を抽出・インデックス化する必要がありました
  3. スクレイピングしてデータの解析に用いる用途であれば、問題はなさそうではある。
  4. 後に規約系は日本語バージョンを優先することにして対処しました
  5. 間違え方ごとにファイルパスを与える関係で、S3ファイルに大量に細かいファイルを上げています
  6. といっても100円弱/月 程度。
  7. Chrome の色覚特性をシミュレーションする機能や、ウェブアクセシビリティ導入ガイドブックなどが参考になりました