この記事はClaude (Sonnet 5 / Fable 5) と一緒に執筆しました。
つくったもの
自分で作ったAndroidアプリ(APK)を、知り合いにだけ配りたいことがありました。
Google Playでの個人アプリの公開は敷居が高いので、配布サイトを作ることにしました。
- アプリが増えるたびに配布ページを都度用意したくない
- 誰でもダウンロードできる状態にはしたくない(Basic認証程度でいいので絞りたい)
- できればお金をかけたくない
という条件で、Cloudflare Pages + R2 だけで動く小さな「配布ポータル」を作りました。
全体構成
設計の軸は次の3つです。
-
R2バケットにパブリックアクセスを与えない。Pages Functionのバインディング
(context.env.APPS_BUCKET)経由でしか読み出せないため、直URLを推測されても
何も返りません。 -
Basic認証はPages Functionですべてのリクエストに対して評価する。
config/apps.jsonに登録していないprefixは常に404を返す「フェイルクローズ」です。 -
パスワードは平文で保存しない。
config/apps.jsonにはSHA-256ハッシュだけを書きます。
アクセス制御ロジック
functions/_middleware.ts から呼ばれる判定ロジックは、骨組みだけ取り出すと
次のような形です。
function evaluateAccess(pathPrefix: string, authHeader: string | null) {
const app = apps.find((a) => a.prefix === pathPrefix);
if (!app) return "DENY_NOT_FOUND"; // 未登録prefixは問答無用で404
if (!isValidBasicAuth(authHeader, app.basicAuthUsers)) {
return "DENY_UNAUTHORIZED"; // 401
}
return "ALLOW";
}
この判定だけを lib/access-control.ts に切り出してJestで単体テストしています。
Pages Function本体(_middleware.ts)はこの関数を呼ぶだけの薄いラッパーです。
「配信経路の配線」と「認可の判断」を分けておくと、テストしたい部分だけを
HTTPから切り離してテストできます。
設定投入時のバリデーションも用意していて、
- prefixの形式が不正
-
basicAuthUsersが空 -
passwordHashがSHA-256ハッシュ(16進64文字)の形式でない
といったケースはビルド自体を失敗させます。壊れた設定はそもそもデプロイまで到達しません。
新しいアプリを追加する手順
-
パスワードのハッシュ値を計算する(平文はどこにもメモしない)。
npm run hash-password -- user1 "実際に使う長くランダムなパスワード" -
config/apps.jsonにエントリを1件追加する。{ "prefix": "new-app", "basicAuthUsers": [ { "username": "user1", "passwordHash": "<上のコマンドの出力>" } ] } -
public/index.htmlにリンクを1行追記する(ここだけ手動更新)。 -
npm run validate-configを通してからpush。Cloudflare Pagesと連携済みなら、
pushするだけで自動ビルド・デプロイされます。 -
各アプリ側のリポジトリは、割り当てられたprefix配下にR2のS3互換APIで
自分のCI/CDからアップロードするように組みます。ポータル側はprefixを
割り当てるだけで、アップロード用の認証情報の発行には関与しません。
良かった点・割り切った点
-
静的サイト+Functionだけで完結するので、サーバー管理が不要で
ランニングコストはほぼゼロ。Cloudflare Pagesのビルド・配信自体が無料枠内で、
R2も「エグレス無料+月10GBストレージ・月100万リクエストまで無料」という
枠があり、知人数人がアプリを落とす程度のアクセス量ではまず課金が発生しません。
個人の配布用途にはちょうどいい規模です。 - ランディングページ(
public/index.html)は、アプリを追加するたびに
手動で1行足す素朴な運用です。数個〜十数個の規模なら、自動生成の
仕組みを作って保守するコストのほうが高くつくと判断しました。 - Basic認証はブラウザ標準のダイアログが出るだけで、UXとしては最低限です。
まとめ
- Cloudflare Pages + R2 + Pages Functionだけで、認証付きの限定配布ポータルが無料枠内で作れる
- 「未登録prefixは常に404」「バケットは非公開のまま、バインディング経由でのみ読む」というフェイルクローズ設計にしておくと、設定ミスがあっても安全側に倒れる
- アクセス制御ロジックを独立した関数に切り出しておくと、単体テストがしやすく、ミドルウェア本体を薄く保てる
「知り合い限定でファイルを配りたいだけで、ちゃんとした認証基盤を建てるほどではない」というときの選択肢のひとつになれば幸いです。