リリースイベントの

アンケート 技術構成編です。

→ 本編はこちら


今回の言い訳__✨️

先に白状しておきます。

このアンケート、
実は機能してない部分がありました。


ちゃんと動いて、
ちゃんと分析もできて、
実際にセットリストも
現場のいろんなことも
改善できたんですけど、


このブログを書くために
中を見直したら、
動いてると思ってた機能が
動いてなかったことが判明しました。


手動大好き人間すぎて
自動機能が動いていない事に
自分で気づけてなかった。


個人開発だから
運用できてただけという。


そのへんも全部書きます。


というのもこのアンケート、
思いついた日から実行したい日まで
5日しかなく、
1日、いや数時間で作ってしまったので
技術選定も理解も甘かったなと
ブログを書いてて思いました。

あとただの静的Webサイトではなく
皆さんに書き込んでもらうシステムなので
今まで作った中で
いちばん複雑な感じがしています。


AIと噛み砕いても
噛み砕ききれてないな?
軽く理解できた気はしても
説明までは出来ないよな
と思ってる部分も多々、、。


そしてこの記事は
専門用語をそのまま使います。


ただ、私はその用語を
人に説明できるほど
完全には理解していません。


AIに教えてもらいながら、
調べながら、
「たぶんこういうことだろう」
または勉強しているAWSの
ここに当たるってことだよね?で
進めている部分がたくさんあります。


なので詳しい方が読んだら
「そこ違うよ」があると思います。
どんどんツッコんでください!

あと、
セキュリティに関わる
キーやURLは伏せています。

・どんなアプリか

簡単な流れはこうです。

参加者
 │ 会場のQRを読む
 ▼
アンケートに回答
 │ 送信
 ▼
その日限定ボイスが聴ける

運営(私1人)は管理画面から

・イベント作成
・質問の編集
・配布ボイスのアップロード
・QRの発行
・回答のリアルタイム集計
・データの保存、削除

を全部やります。

Cloudflareのダッシュボードを
毎回触らなくても回ることが
絶対条件でした。


実際のアンケート画面のスクリーンショット



・全体構成

参加者スマホ / 運営スマホ
  │
  ▼
Cloudflare Pages
 … 画面(React)
  │ /api 通信
  ▼
Cloudflare Workers
 … API(Hono)
  │
  ├─ D1 … データベース
  ├─ R2 … ファイル(音声・CSV)
  ├─ KV … 短期カウンター
  └─ Turnstile … Bot対策

全部Cloudflareの上で動いています。
いわゆるサーバーレス構成。

・PagesとWorkersを
分けている理由

ここ、書きながら自分でも
整理がついたところです。

Pagesは「配るだけ」。


誰が来ても同じファイルを返します。

AWSで言う
CloudFront+S3の構成ですね。

Workersは「処理する」。


リクエストのたびに
DBを読んだり書いたり。

今回は回答を保存する必要があるので、
配るだけでは足りない。


これの理解難しくて
AWSなら何?と
AIに説明してもらったら

APIサーバーとしての役割で言えば
「API Gateway + Lambda」なんだけど
動く場所で言えばエッジなので
Lambda@Edgeに近いってことだよと
教えられたけど、、

何となく大枠まぁ
わかったかもしれないよぉう、、
って感じです😵‍💫


なので

・参加者が見る画面 → Pages
・回答を保存する、
 この端末は回答済みか確認する、
 音声を返す → Workers

という分担ですね。

・使っている技術

フロント(画面側)

React 18
Vite
TypeScript
Tailwind CSS
React Router
Recharts(グラフ)
qrcode(QR生成)

多いなぁ、、

コードはもちろん1行も書いてないので
画面みて、ここはReact!
ここはTailwind CSS!とか言えません。
ダサい🙂言えるようになりたい。

参加者向けのアンケート画面と、
運営向けの管理画面の両方が
ここに入っています。

バックエンド(API側)

Cloudflare Workers
Hono(軽量Webフレームワーク)
Zod(入力チェック)

Honoは、
どのURLに来たらどの処理をするかを
振り分けてくれる部品です。

整理整頓振り分け係君ですね!

Zodは、送られてきたデータの検査係。

「年代はこの選択肢の中のどれか」
みたいなルールを先に書いておくと、
変なデータが来たときに
弾いてくれるんですね。

これがないと直接API叩かれて
年齢50000歳とか
出身地尸魂界とか
出来ちゃうということ、、


面白いけどダメー!


データまわり

D1 … イベント / 質問 / 回答 / 集計
R2 … 配布音声・CSVバックアップ
KV … レート制限・R2使用量カウント

(このCSVバックアップ、
後半でとんでもないことになります)

・なぜNext.jsを使わなかったか

これはAIくんの判断です😂

軽く。安く。壊れにくい。

このアンケートは
イベント当日の数時間しか動かない。

その代わり、
その数時間は絶対に落ちてはいけない。

やることも
「フォームを表示して、保存して、
音声を返す」だけ。

だったら
Vite+Workersの
シンプルな構成で十分だし、
部品が少ないほど
壊れるところも少ない。

そう判断し動いたそうです。

・正直に書くと、
全く比較しきれていません

このアンケート、
思いついたのは
1回目のリリースイベントが
終わったあとでした。

次のイベントまで5日。


なので、ほぼ1日、
というか実質数時間で
爆速で作りました。


その結果どうなったかというと、
技術の選定を
ほとんど比較していません。


ああ、そんな感じね
大丈夫だと思うなぁ(小並感)
の連続でした。


たとえば今回、
画面はPages、APIはWorkersと
分けて作っています。


でもCloudflareには
Pages Functionsという、
Pagesの中でAPIも動かせる
仕組みがあります。


このブログを書きながら調べていて、
今回の規模なら
そっちもよかったのか、、?
と思いました。

デプロイも1回で済むし、
設定も減ります。


後で出てくる
「プレビューURLでログインできない」
問題も、
そもそも起きなかったかもしれません。

(同じプロジェクトの中にいれば
別サイト扱いにならないので)

もちろん、
分けていることの利点も
あるんですね。


画面とAPIを別々に更新できるし、
APIだけ他から呼ぶこともできる。


うーん。
やはりAIの選択の方が正しいなと
ブログ書きながら感じました。


ただ、
比べた上で選んだのか、
比べずに選ばれていたのかは
まったく別の話です。


今回は完全に後者でした。


かなり細かい仕様書まで
作りこんでないと
AIって最初の技術構成を
ひっくり返す判断を
なかなかしてくれないような
印象があるので、

ChatGPTとの壁打ちからの
Claudeでの仕様書落とし込みが
効いたなと思います。


今回は動いたので
結果は良かったのですが、
次は「なぜこっちなのか」を
もっと理解して作るように
しないとですね。

もしやこれは
AIで開発する人が
一番落ちやすい穴では、、?
と思います。


動くものはすぐ出てくるので、
選ばなかった方の選択肢を
そもそも知らないまま
進んでしまい、

本当は違う構成がハマるのに
チャットのやりとりでAIに
技術構成ひっくり返すまでの
プロンプトを提出できず
AIも人間もなんか上手くいかないなと
ぼんやりしながら構成変えず
作り続けてしまい
無駄なトークン使っちゃった…
みたいなことは起きそうです。


気をつけないとなぁ。

やはり勉強が大事。

・Cloudflareの各サービスの役割

Pages(画面配信)

Reactで作った画面を配信します。

参加者URLはこんな形です。


https://(ドメイン)/survey/(イベントID)

Workers(API)

APIサーバーです。





/api/... へのアクセスを受けて、
D1・R2・KV・Turnstileと
やり取りします。

D1(データベース)

SQLiteベースのDBです。


主なテーブル

events イベント
question_templates 質問テンプレート
template_questions 質問
responses 回答本体
response_answers 質問ごとの回答
event_stats 保存済み集計
system_meta システム情報

R2(ファイル保存)

アンケートで使っている
バケットは1つだけです。


hanakin-voices … イベント配布音声

音声はイベントごとに1ファイル。
回答者全員が同じ音声を聴きます。

(設計上はここに
CSVバックアップも
入るはずだったんですが、、
その話は後で)


KV(短期カウンター)

送信レート制限(IPごと)
R2の月間操作回数

消えても困らない数字だけ
ここに入れています。


無料枠を超えないための

独自の上限管理にも使っています。


Turnstile(Bot対策)

送信前に人間確認をします。

・ちなみにAWSで言うと

AWSを勉強しているので、
自分の整理も兼ねて。


D1   ≒ RDS / Aurora
KV   ≒ DynamoDB
R2   ≒ S3
Pages  ≒ S3 + CloudFront
Workers ≒ API Gateway + Lambda
     (動く場所はエッジなので
      Lambda@Edgeにも近い)

ってところかな、、?


D1 KV R2 Pagesは
考えなくても置き換えられたけど
Workersの理解が浅い。
浅瀬漂い過ぎてる。

・データ設計でこだわったこと

個人情報をできるだけ持たない

・氏名、連絡先は取らない
・都道府県、年代は任意
・IPアドレスやブラウザ情報は
 そのまま保存せず、ハッシュ化して保存

1端末1回答

device_fp = ハッシュ
( 端末ID + IP + ブラウザ情報 )

完全な本人確認ではないですが、
会場アンケートとしては
十分な重複防止に
なっているかなと。


ここは回答出来ない!とか
なるとまずいので
ちゃんと見ました。

回答したらボイスが聴ける仕組み

ボイス取得APIは、
その端末が回答済みかを
毎回チェック。


未回答の端末が
直接URLを叩いても、
403で弾かれて
ボイスは取れないように。


特典に関しても
基準を満たしてない方には
配られないようになってるかは
かなり大切に思ってます。


ズルは良くないぞぉー!

・イベントのライフサイクル

イベントには状態があります。

draft     下書き(公開前)
open     受付中
closed    受付終了
archived   集計保存済み
data_deleted 生データ削除済み

サービス側にデータを
たくさん保存しちゃうと
課金されちゃうゾーーってところは
AWSのUdemyで
飽きるほど教えてもらったので、

CSVに書き出して手元に置いたら、
サーバー側からはすぐ消す。


AWSのS3バケット
ライフサイクルルールみたいだ!
とか考えてた😂


で、消すときの順番が大事なんです。


1. CSVをR2に保存
2. 保存できたか確認
3. 集計を保存
4. それが全部成功してから生データ削除

CSV保存に失敗したら、
生データは絶対に消さない。

、、という設計でした。


ここから、
冒頭で言った「機能してなかった」話に
なります。

・Cronの話
(前の記事の訂正です)

本当は
私は保存すら忘れる可能性あるので
定期実行もよろしくね!と
やりたかったんです。


でも前の記事で書いた通り、
配信システムを作ったときに
こういうエラーが出たんですよね。

Cron triggers are not supported
on the Workers Free plan

あー有料か、
じゃあ諦めよ、となって、
手動トリガー方式に切り替えました。


ところが今回、
このブログを書きながら
Cloudflareを見に行ったら、

アンケート側のCron、
普通に登録されてました。


Cron triggers
0 18 * * *

毎日1回。

(UTCなので
日本時間だと午前3時です)


管理画面でも出来てる風になっていた。




課金してるのはR2だけで、
Workersは無料プランのままです。


なんで?と思って調べたら、
Cron Triggersは2020年の登場時から
無料プランでも追加料金なしで使えると
Cloudflareが明言していました。


つまり最初からずっと無料。

じゃあ、あのエラーは何だったのか。

分かりません。


外部APIを叩くCronだから、、?とか
考えてみたんですけど、
Cron側にそういう区別は
無いっぽいんですよね。


分かる方いたら教えてください。


ただ、原因は分からなくても
私が何を間違えたかは
はっきりしています。


エラーメッセージを見て
「そうなんだ、じゃあなしで!」だけで
止めたこと。


調べ直さなかった。


APEXのパッチノート読むんだから
開発でも読んでくださいね私。


Cloudflareにも
ちゃんとChangelogありました。


RSSも出てます。

読みます。これから。すみません。。

・そして、もっとひどい話

Cronが動いてるなら、
CSVバックアップも
勝手に保存されてるはずですよね。

なので見に行きました。


R2のバケット。


hanakin-voices … 24ファイル


、、、無いですねぇ~?

CSVを保存するはずの場所が、
そもそも存在していませんでした。


つまり
「動くはずの処理が登録されていて、
実は何も保存されていない」
状態だったということです。


エラーも出ません。
誰も教えてくれません。

私が今日、
ブログを書きながら見に行くまで
一度も気づきませんでした。


自分のシステムなのに。


後で「エラー監視の仕組みがない」を
課題に挙げるんですけど、
まさにこれです。

偶然壊れなかったんじゃなくて、
壊れてても分からなかった。

おぉーこわこわ。


個人開発で楽しんでる程度だけれども
回答してくださってる方が
いらっしゃるシステムなので
抜けがあるのは
本当に良くないことです。


ちなみに
回答データ自体は全部無事です。


なぜかというと、
毎回しっかり名前を付けて
手動で保存して、
分析してたから。


今回は
自動保存の方を
手動のバックアップとして
捉えていたんですよね。


自動が本命で手動が保険、
じゃなくて、
手動が本命で自動が保険。

結果的にそれが効きました。


これからどんどん
作れるものが増えていって
定期実行システムを作るのが
上手になっても、
自動を過信しない初心は
忘れないでいたいなと思います。


動いてるように見えるものが
動いてるとは限らない。


今日それを
自分のシステムで教わりました。

・作った順番

ステップ1|まず動くものを作る

・アンケート画面
・回答保存
・ボイス配布

の最小構成だけ作りました。


この時点では質問は固定、
ボイスもランダム配布でした。


ステップ2|運営が自分一人で回せるように

毎回コードを触るのは無理なので、
管理画面を作り込みました。


・イベント作成
・質問の追加、編集
・ボイスのアップロード
・QR発行
・集計グラフ


質問はテンプレートから
複製する方式にして、
イベントごとに自由に変えても
他のイベントに
影響しないようにしました。


ステップ3|本番リリース

D1、R2、KV、Pages、Workersを作成し、
シークレットや環境変数を設定して公開。

ステップ4|実運用しながら改善

ここからが本番。

・つまずき&改善ログ

ここは自分の手で
しっかり直して行ったところ。


① スマホで横スクロールが出る

管理画面がスマホで見づらかった。

→ 全体に overflow-x: hidden、
表はスマホだとカード表示に切り替え。 


② 時刻がJSTとUTCでズレる

「7/1のイベントが開かない」問題。

サーバーはUTC、
日本はJSTで9時間ズレる。

AIくん、すーぐ
時間バグバグするんですよ、、

こういう所がアマチュア極めてて
仕様書で落としてるから
なんだよなぁああ。プロりたい。

→ 日時比較を全部
「日本時間ベース」に統一。


③ iPhoneのボイスメモが上がらない

.m4a が弾かれていた。

→ 許可する形式を増やし、
.m4a を audio/mp4 として
扱うよう修正。

④ 「Bot確認して」から進めない

Turnstileの表示タイミングの問題。

→ 明示的に表示させる方式に変更し、
トークン失敗、期限切れの処理も強化。


⑤ 管理画面の質問修正が即反映されない

キャッシュが原因。

→ APIリクエストを毎回
キャッシュ無効にし、
保存後に自動で再取得するように。


⑥ プレビューURLでログインできない

デプロイごとに出る一時URLだと、
APIが別サイト扱いで弾いていた。

→ 同じプロジェクトのプレビューURLも
許可するようにAPI側を調整。

これもCORSだよねぇ、、


⑦ 流入グラフが分かりにくい

円グラフだと
どこが強いかが読みにくかった。

→ ランキング表示
(順位+件数+割合+バー)に変更。

さらにInstagram、Xなどの
媒体単位で1カウントずつ
集計するようにして、
組み合わせで
数えないようにしました。


⑧ 「良かった曲」が集計されていなかった

回答は保存されていたのに、
管理画面に出していなかった。

→ 曲ランキングを追加。

さらに初参加/リピーターで
どの曲が刺さったかを
比較できるようにしました。

本編で書いたクロス集計は、
ここでやっと
見られるようになったものです。


⑨ 完了画面をリロードするとボイスが消える

画面遷移時のデータだけに
頼っていたため。

→ リロード時はサーバーから
取り直すように修正。

⑩ そして今日見つかったやつ

CSVバックアップが保存されてない。

→ これから直します。

・データを「使う」ための設計

ただ集めるだけでなく、
次にどうするかに使えるように
質問を途中で設計し直しました。


たとえば

・CD予約したか / 見送ったか
・予約の決め手 / 見送った理由
・どの瞬間に気持ちが動いたか
・また来たい理由
・SNSをフォローしているか

こういう選択式の質問を足すと、

満足度 × 予約
初参加 × リピート意向
流入元 × フォロー状況

みたいなクロス分析ができます。

回帰分析や決定木などの統計解析も
しやすいように、
なるべく選択式にしています。

自由記述は最小限。
スパム対策の意味も込め500文字制限。
回答率を落とさないためです。

・セキュリティと
「事故らない」工夫

・管理APIは全部トークン認証
・Bot対策
 (Turnstile+honeypot+最低滞在時間)
・同じIPからの連投を制限
・CSVの数式インジェクション対策
・セキュリティヘッダー(CSP)を設定
・無料枠を超えないよう
 R2操作に独自上限

CSVの数式インジェクションだけ補足を。

CSVをExcelで開くと、
= で始まるセルを
数式として実行してしまいます。


自由記述に =1+1 と書かれたら、
Excelでは 2 と表示される。

もっと悪いものを書かれると
外部への通信を
仕込むこともできる。


なので = + - @ で始まる値は
ただの文字として扱うように処理。


ここは仕様書制作の段階で
自由記述で悪いことする人来ても
大丈夫なようにして!と
お願いしてるのが効いてますね!


さらに

・ビルド前に環境変数を自動チェック

環境変数のチェックは、
GitHubにコードを上げたあと、
ビルドが始まる一番最初に
やってくれています。

Turnstileのキーや管理トークンが
1つでも空なら、
そこでビルドを止める。


止めないとどうなるかというと、
ビルドは成功して、公開もされて、
本番でだけ動かない。


しかもエラーが出ないまま
無言で壊れることがあると。


当日それが起きたら詰むので、
先に止めてくれています。


疲れて深夜に1人で運用してても
事故らない仕組みにして!!と
仕様書制作時点でかなり言ってます。


レビューでもセキュリティだけじゃなく、
これってボケっとしてて
こんな風に操作しても壊れない?とか、
ほんとに最後まで
ひとりで運用できるの?とか
中のデータ取れちゃう方法ない?とか

多分人だったら
ウザがられるレベルで何度も聞く、
違うAIでも聞く。


これもそれがとても効いてますね!

、、と書いておいて
CSVが保存されてなかったんですけどね。

「事故らない設計にして」と
何度も言ったところで、
それが本当に動いてるかは
別の話でした。


言えばやってくれるんじゃなくて、
言ったことが動いてるか
自分で見に行かないとダメ。

一番の学びかもしれません。


・今後の課題

正直に書きます。

第一に「私、もっと勉強してください!」

・自分のシステムの状態を
 把握できていない
 今日のCSVの件がまさにそれ

・エラー監視の仕組みを作ってなかった。
 偶然壊れなかったに過ぎない。
 今は不具合があっても
 自分が気づくまで分からない。

・フィンガープリントは万能ではない
 どうしても回避されうる方式。

・テストコードがない
 毎回手で確認しています

・複数イベント同時開催の負荷は未検証

1人イベント想定で
動かしてしまっているので
展開する気はないですが。

・作ってみての学び

・サーバーレスは個人開発と相性が良い
 安い、スケールする、管理が楽

・時刻は最初に基準を決めるべき

・実運用しないと分からない不具合が大半

・管理画面のUXは
 作り手の自分がいちばん助かる

・データは「集める設計」より
 「使う設計」が大事

・動くものが早く出るほど、
 選ばなかった選択肢を
 知らないまま進む

・エラーメッセージを鵜呑みにしない。
 公式のChangelogを読む

・「作った」と「動いてる」は別物。
 自分で見に行く

・自動を過信しない。
 手動を保険じゃなく本命に置く


技術的にはシンプルです。

現場で1人でも回せること。

参加者が迷わないこと。


その2つを最優先に組み立てた、
というのがこのアプリの
特徴だと思います。


機能してなかったところも含めて、
全部書きました。


個人開発では
しっかり免責事項、ポリシーを
表示した上で、
個人情報流出など
トラブルに発展しそうなところは
鬼の想像力を働かせて
もともと絶対に触れない!


ド初心者なので
鬼の想像力で
上手くいかなかったねぇクスクス🤭
で済まされるであろう部分しか
開発しない!!


アイドルのトークと一緒で

回収出来ないなら広げない、、!

(宮本がトーク回収できてるかは微妙)


でも鬼の想像力が働かなかったところで
何かが起きてしまう可能性も
ゼロではないので
鬼勉強する。


これに尽きますね、、!!!

・構成まとめ

[参加者スマホ]
  │ QR
  ▼
[Cloudflare Pages / React]
  │ /api
  ▼
[Cloudflare Workers / Hono]
 ├─ D1 : イベント・回答・集計
 ├─ R2 : 音声・CSV
 ├─ KV : レート制限・使用量
 └─ Turnstile : Bot対策
  ▲
  │ 管理(トークン認証)
[運営スマホ / 管理画面]


以上です。

同じようにCloudflareで
個人開発したい人の
参考になれば嬉しいです。


音声アップロード画面はこんな感じでした~!



→ 本編はこちら


→ 宮本佳林の音楽活動はこちら



PS.

宮本佳林、実はミュージカル稽古に
入るので開発ブログ更新は
一旦ゆるやかになります!

たくさんの方が見てくださっていそうなタイミングで多めに更新をしたく頑張っておりました😂


また更新したらXなどでお知らせするので
ぜひ見に来て頂けると嬉しいです!

、、アイドル活動も
是非見て貰えると嬉しいです😂