⚔️

きのこたけのこ論争から本格投票サービスをAWSで個人開発した話

に公開

はじめに

「きのこ派?たけのこ派?」

この質問を投げかけられたとき、あなたはどう答えますか?

僕は迷わず「たけのこ派」と答えます。理由は単純で、あのサクサクのクッキー生地とチョコレートのハーモニーが大好きだからです。でも、きのこ派の人たちは「いやいや、クラッカーのパリパリ感こそが至高」と譲りません。
そんな不毛な(?)論争を何度も繰り返すうちに思いました。

「これ、ちゃんと決着つけられるWebサービス作ったら面白くない?」

この疑問を元に開発したのが、投票プラットフォーム「Opiniqa」です。

https://opiniqa.com/

今回は、この開発を通じて学んだことや苦労話を、赤裸々に語ってみたいと思います。

なぜ作ろうと思ったのか

きのこたけのこ論争って、ずっと昔から耳にしたことがある人が多いと思います。調べたところ、かの明治も直近で2020年に「きのこの山たけのこの里国民大調査」という大規模国勢調査をやったりしていて、この論争を楽しんでいます。

「あれ、公式がすでにここまでやってるなら開発する必要ないのでは…」

脳裏にそのような考えも浮かびましたが、落ち着いて現在インターネット上で投票を使えるサービスについて考えを巡らせました。
Xのアンケート機能はとても便利だし自分もたまに使うけど、選択肢が4つまでしかないのが辛い。また投票期限も最長で7日間先までしか設定できない。
Yahoo! ニュースの「みんなの意見」は、投票自体は面白いもののニュースに対してトピックが紐づく形になっていて、自分からトピックを立てることができない。

そんな考えから、既存の投票サービスには物足りなさを感じていました。

  • 選択肢の制約が強い
  • 結果が共有しづらい
  • 議論ができない

僕が作りたかったのは、もっと 「楽しく語り合える場所」 でした。きのこ派もたけのこ派も、お互いの推しポイントを存分に語って、でも最終的には「どっちも美味しいよね」って腹落ちし合えるような。

そんな理想を実現するために、本格的な投票・議論プラットフォームを作ることにしました。

技術選定の舞台裏

開発の相棒: 生成AI(特にClaude)

ここ1年ほどで飛躍的に扱いやすくなった生成AIですが、これによって開発スピードが文字通り爆速になりました。

最初はOpenAIの4oを使って「DynamoDBのテーブル設計はこれでいいかな」「POSTメソッドで受け取ったhogehoge_idを使って〜〜するLambda関数を書いて」みたいな感じで書いてもらってはコピペするという感じで主に進めてたんですが、途中からClaude Codeが使えるようになって、これがもう革命的でした。

デバッグする時も「このエラーなんで出てるの?」と聞くと、実際のコードを見ながら「ここの型定義が間違ってますね」って指摘してくれるし、「こんな機能追加したい」って相談すると、既存のコードを理解した上で適切な修正を提案してくれる(しばしばボロが出るけど)。

認証周りを初めて実装する時、Claude Codeが「この順番でこう処理すればOAuthを通せるはず」って具体的なコードを提示してくれたときは、「未来すぎる...」って感動しました。

全体設計:サーバレスアーキテクチャ

個人開発において最も重要な方針の一つがコストを低く抑えることです。コストを抑えることで次のようなメリットがあります。

  • 単純にお金がかからないので嬉しい
  • サービスを長期運用することができる(→利用拡大のチャンスが増える)
  • 急に利用者数が伸びた時に焦らなくて済む
  • コスト感覚が本業にも活きる

よりコストを抑えて運営するため、Opiniqaでは、AWSのサーバレスサービスをのみを使用してサービス稼働させてます。具体的には以下のサービスを利用してます。

  • AWS Amplify:Next.jsアプリのホスティング
  • API Gateway:Lambdaへの橋渡し
  • Lambda:DynamoDBへの橋渡し 兼 諸々の数値処理
  • DynamoDB:データ保存
  • Route 53:ドメイン取得、DNS設定

フロントエンド: Next.js + MUI

最初はReactの素のプロジェクトで始めようと思ったんですが、どうしてもXでURL表示した時にOGP画像を表示させたくなって途中からNext.jsを導入しました。ReactベースからNext.jsベースへの移行も生成AIにやってもらいました。

UIライブラリにはMUIを選択しました。MUIを選んだ理由は、デザインセンスが皆無な僕でもそれなりに見栄えの良いUIが楽に作れること、ベーシックなUIコンポーネントなので(いい意味で)ある程度枯れていそうだったこと、の2点です。
特に投票結果のグラフ表示にもMUI Chartsを使いたかったのですが、細かいカスタマイズがしきれないこと、グラフ表示の見え方はサービスの根幹に関わることから、最終的に自作でこしらえる形としました。

バックエンド: AWS Lambda + DynamoDB

ここが一番の厄介ポイントでした。最初は「LambdaいくつかとDynamoDB数テーブルでいけるでしょ」と思ってたんですが、気づいたらLambda関数は約50個も管理することになってました。
通常のCRUDを全て用意するだけである程度かさむのですが、なぜこんなに増えたかというと

  • トピック系、投票系、コメント系: Readで特定トピック、特定ユーザー等文脈に合わせたリクエストを用意した
  • ユーザー系: OAuthログインやユーザー認証のための処理も全てLambda関数でカバーした

「もうちょっと機能を追加したい」が積み重なった結果です。段取り力の弱さが丸見えになる形となってしまいました。

API GatewayはHTTP APIで構築しています。通常であればREST APIで構築した方が便利なのですが、とことんコストを抑えようとしていたためおおよそ1/3程度のコストで済みそうだったHTTP APIを選択しました。
REST APIを選択すれば、単純な操作はLambda関数なしで操作できるためここまで増やす必要はありませんでした。
API Gatewayは100万コールで数百円程度のオーダーなので、そこまで気にせずにREST APIで作成すればよかったな、とシンプルに後悔しています。

DynamoDBですが、個人開発で利用する場合はあえて設定したい目的がある場合は除き、基本的にはプロビジョンドキャパシティではなくオンデマンドキャパシティとした方が良いです。 プロビジョンドキャパシティはその名の通り一定のキャパシティをサービス用に確保し続ける形になるため、利用料が無料利用枠をはみ出しやすくなります。

認証基盤での悩み

最初は「認証なんてCognitoでサクッと済ませよう」と思ってたんですが、料金を調べてびっくりしました。
Cognitoは月間アクティブユーザー(MAU)が50,000ユーザーまでは無料なんですが、問題はその後です。
東京リージョンでは、50,000ユーザーを超えた分について1MAUあたり$0.0055 USDの料金がかかります。つまり、55,000ユーザーになったとき、超過分5,000ユーザー × $0.0055 = $27.5 = 約4,000円の月額料金が発生します。

https://aws.amazon.com/jp/cognito/pricing/

個人開発で「もしかしたらバズって急にユーザーが増えるかも」と考えると、50,000ユーザーの壁を超えた瞬間に急に月額料金が発生するのは、正直怖いですよね(嬉しい悲鳴ですが)。

しかも、利用規模が大きくなればなるほど、この料金は一気に跳ね上がります。10万ユーザーなら月額約40,000円、20万ユーザーなら月額約120,000円...エンタープライズ向けで月数百万円レベルの規模のシステムであれば許容できるかもしれませんが、雀の涙程度の予算で活動している個人開発では一発アウトです。

そこで、「認証機能はOAuthを用いて自前で実装する」 という判断をしました。Twitter(現X) OAuthの実装は大変でしたが、長期的にはコスト面で安心できる選択だったと思います。

開発中のエピソード

Twitter OAuth認証

TwitterのOAuth認証、これが大変でした。
最初は「ログイン機能なんて簡単でしょ」と思ってたんですが、実際にやってみると:

  1. リクエストトークンの取得
  2. ユーザーをTwitterの認証ページにリダイレクト
  3. コールバック処理でアクセストークンを取得
  4. ユーザー情報を取得してセッション管理

この一連の流れを、3つの異なるLambda関数で処理する必要がありました。しかも、セッション管理のためのトークンをDynamoDBに保存して...

ある程度の雛形は生成AIで実装してくれたため数日かかったりなどはありませんでしたが、個別カスタマイズする部分でコケることが少しありました。

デバッグ中は、認証の途中で止まってしまって「あれ?どこで失敗してる?」と1日レベルでログとにらめっこしたことも。

匿名投票の実装

投票をログイン済み状態のみにしてしまうと気軽に参加できないと思い、未ログイン状態での投票機能も追加しました。
ただ、同じ人が何回も投票するのは防ぐ必要があったため、そのような仕組みも作りました。厳密には完璧ではないけど、ちょっとしたいたずら防止にはなります。
実装の詳細は見送りますが、個人開発レベルでは十分な効果を発揮してくれています。

画像生成サービスの追加

トピックの情報や投票状況を反映させた上でOGPの画像として表示させるために、Python + PIL + OpenCV で画像生成サービスを作りました。

これをDockerコンテナ化して、Lambda関数から呼び出す形にしたんですが、ピクセル単位でcv2.putTextやcv2.rectangle等の画像処理の手順が思った以上に複雑になってしまいました。(おまけにこの部分は生成AIが苦手)
ですが、自動生成されたトピック画像がSNSでシェアされたときの見栄えは格段に良くなりました。

ドメイン取得

ドメインは最終的にRoute 53で取得しました。有名なサービスとしては他にお名前.comやCloudflareがあると思います。
特に国内だとお名前.comが有名ですが、以前ドメイン取得した際にUIが非常にわかりづらく気づかないうちに知らないオプションを申し込んだり、5年の定期契約になっていたりして意図せず費用が発生した経験がありました。
当時苦い思いをしたことから、今回は操作もシンプルなRoute 53で取得しました。

技術的な学び

Lambda関数の管理

約50個ものLambda関数を管理するのは、想像以上に大変でした。

命名規則を統一して、機能ごとにディレクトリを分けましたが、それでも「あれ?この機能どの関数だっけ?」となることが多々ありました。デプロイは手動で行っているので、現在では更新作業も結構な手間になってしまっています。

次回があるなら、もう少し関数をまとめるか、API GatewayをREST APIにするか、GraphQLで統一するか。などしたいですね。

DynamoDBのキャパシティーユニットの落とし穴

DynamoDBはキャパシティユニットという単位でコストが発生します。そしてキャパシティユニットは読み込み時と書き込み時で利用量が異なり、読み込みキャパシティーユニットは4KBごとに、書き込みキャパシティーユニットは1KBごとに消費されます。

ここが重要で、DynamoDBはJSONのような形式でデータを保存するので、各レコードごとにカラム名が繰り返し記述 されます。

具体的にはこんな感じです。

# 悪い例: 長いカラム名
{
  "very_long_column_name_for_user_identification": "user123",
  "another_very_long_column_name_for_voting_topic": "kinoko_vs_takenoko",
  "extremely_long_column_name_for_comment_content": "きのこ派です!"
}

# 良い例: 短いカラム名
{
  "uid": "user123",
  "q": "kinoko_vs_takenoko", 
  "cmnt": "きのこ派です!"
}

したがって、可読性を重視してカラム名にuser_identificationとかvoting_topic_idみたいな長い名前をつけると、これが積み重なってキャパシティーユニットの効率がかなり悪化してしまいます。

幸いそこまで長いカラム名はなかったため大きな影響はなかったのですが、事前に知っていればもう少し短い各カラム名を設定していたと思います。

他にも、RDBであれば簡単に検索できるものがDynamoDBだと難しい場合があります。この部分は定期的に悩みに直面していて、機会があれば別記事としてまとめていきたいです。

フロントエンドの状態管理

投票状態、ユーザー認証状態、コメント表示状態など、管理すべき状態が思った以上に多くて、途中からContext APIを使って整理しました。

状態管理をするにあたってはRedux等の専用ライブラリも考えられます。
そのような状態管理ツールを採用しなかった理由もあり、今回フロントや認証など初めて本格的に触る要素が既に多く、未知のツールに手をつけて溺れるよりもまずはリリースまでたどり着くことを優先したかったため、シンプルにReactのContext APIを利用するに留めました。

また、状態管理は実装するにあたってユーザーの利用動線を順番に追って考える必要がありますが、プログラム本文ではそれが表現されていません。そのためプログラム本文をベースに考える生成AIにとって状態管理は苦手な機能分野の一つ でした。

昨今Kiroではより上流部分を主体的に考えてくれるようになっているので、近いうちに状態管理の方も自身で利用動線を検討して設計してくれるようになってくれるだろうと思います。

運用してみて

コスト面

AWS Lambda + DynamoDBの従量課金は、個人開発には本当にありがたいです。
月間数千リクエストレベルなら、無料で運用できてます。RDSのインスタンスを常時起動するより、圧倒的に安い。

最近ではvercelやsupabaseなどサービスをかなり便利にデプロイできるPaaSサービスがたくさん利用できるようになっています。すぐに変更内容をデプロイできる、フロントやDBなど本当に重要なところに集中できる、などメリットは多岐にわたると思います。

ただ、これらのサービスは無料で利用できる枠はスペックがかなり限定的で、ユーザー数が伸びてきた段階で毎月数千円のコストが固定費として発生します。いろんなことをPaaS側でよしなに処理してくれているため、利用料が増えてきた時のコスト削減策として取れる手段が限られてきます。

ユーザー数が増えた状態でも出来る限り費用を抑えやすくする、キツくなったらどこか削減策をとる、というのをしやすくするためにAWSで構築しました。

AWSを選択した他の理由としては、自身でWebサービスを開発することで一通りの知識をつけたかったこと、単純に本業でAWSをよく使っているので比較的スムーズに構築できそうだったこと、があります。

他のインフラの選択肢にGoogle Cloud上でFirebaseを利用するのもあると思います。後から知るとFirebaseも便利そうな話を聞くことがそれなりにあったので、もし自分がAWSとGoogle Cloudの知識が同じくらいであれば、Firebaseを選択していたと思います。

パフォーマンス面

Lambdaのコールドスタートが気になるかもと懸念していましたが、投票サービスの場合は極端にレスポンス速度を求められるわけではないので、現状では特に問題ありませんでした。
むしろ、DynamoDBの安定性とスケーラビリティの恩恵の方が大きく感じています。

今後の展望

これまで「何よりもまずリリースすること」を重視して開発を進めてきたため、実装したい機能がまだまだ実装しきれていません。

より便利に投票、トピック作成ができるよう、またユーザー間でより投票を楽しんでもらえるよう、今後も新機能を引き続き実装予定です。

最後に

「きのこたけのこ戦争を決着させる」という、一見くだらない着想から始まったこのプロジェクトですが、技術的にも個人的にも、本当に多くのことを学びました。

個人開発って、「こんなのあったら面白いかも」という純粋な好奇心から始まって、様々な要素技術について自分ごととしてキャッチアップでき、気づいたら本格的な一つのシステムになってる、みたいなところが本当に面白いと思います。

そして何より、自分の作ったサービスで実際に人が楽しんでくれるのを見ると本当に嬉しいものです。

ちなみに、この記事を読んでくださった皆さん、きのこ派ですか?たけのこ派ですか?

よかったら、Opiniqaで投票してみてください。

https://opiniqa.com/topics/0000000000000001


この記事がよかったら、SNSでシェアしてくれると大変嬉しいです。また、技術的課題に対する対応やサービスへの機能追加要望等についてもぜひコメントにていただけますとありがたいです。

Discussion