ハコヅミを作る #2:雛形・CI・自動デプロイ・Cloudflare Accessまでの初期構築
前回(#1)で紹介したキャンプ道具のチェックリストアプリ「ハコヅミ」の、開発の土台を作りました。
やったことは次のとおりです。
- GitHubのリポジトリ作成と、Issueのテンプレート
- SvelteKitの雛形
- PRごとの自動テスト(CI)
developは開発用アカウント、mainは本番用アカウントへの自動デプロイ- 開発・本番の両方をCloudflare Accessで保護
作業は、決めた開発の流れどおり、土台づくりの段階から「Issue → ブランチ → PR → マージ」で進めました。 実装とPRの作成はClaude Codeが行い、自分はレビュー・判断・マージを担当しています。
1. リポジトリとGitHubのアカウント
GitHubのアカウントは、個人用(練習用のアプリ)と事業用(ブログなど公開するもの)を分けています。 ハコヅミは最終的に事業として公開するので、事業用のアカウントに非公開のリポジトリを作りました。コードを公開するかどうかは、後で決めればよいと考えています。
コミットの作者情報をフォルダで切り替える
アカウントを分けていても、Gitの作者情報(名前とメールアドレス)が1つのままだと、事業用のリポジトリに個人名のコミットが混ざります。
そこで、事業用のリポジトリはすべて1つのフォルダの下に置き、そのフォルダの下でだけ事業用の作者情報を使うようにしました。Gitの includeIf という機能です。
# ~/.gitconfig(全体の設定=個人用)
[user]
name = 個人用の名前
email = 個人用のメールアドレス
# ~/Jiru-Labo/ の下のリポジトリだけ、別ファイルの設定を読む
[includeIf "gitdir:~/Jiru-Labo/"]
path = ~/.gitconfig-jiru-labo
# ~/.gitconfig-jiru-labo(事業用)
[user]
name = Jiru-Labo
email = 事業用のメールアドレス
これで、リポジトリごとに設定し忘れる心配がなくなりました。 ちなみに、設定を確認してみたら、全体の設定が今は使っていないメールアドレスのままになっていました。ついでに整理できてよかったです。
デフォルトブランチは develop
開発プロセスの記事で書いたとおり、PRの Closes #番号 はデフォルトブランチへのマージでIssueを閉じます。
develop にマージした時点で閉じたいので、デフォルトブランチを develop にしました。
その代わり、develop → main のPRを作るときは、向き先を main と明示する必要があります(gh pr create --base main)。省略すると develop 向けになってしまうためです。
2. SvelteKitの雛形と、つまずいた点
雛形は、Svelte公式のCLI(sv)で作りました。技術スタックに合わせて、必要なものをまとめて指定しています。
npx sv create . --template minimal --types ts \
--add prettier eslint "vitest=usages:unit" playwright "tailwindcss=plugins:none" \
"sveltekit-adapter=adapter:cloudflare+cfTarget:workers" "drizzle=database:d1" \
--install npm
一発で済むかと思いきや、いくつかつまずきました。
Node.jsが古かった
npm install が、次のエラーで失敗しました。
npm error Cannot read properties of null (reading 'edgesOut')
原因が分かりにくいエラーです。新しいnpmで試したところ、本当の原因が表示されました。Node.jsのバージョンが古かったのです(ESLint 10が、手元のNode.jsより新しいバージョンを要求していた)。
Node.jsを最新のLTS(24)に更新して解決しました。あわせて、.node-version ファイルと package.json の engines で、Node.js 24以上を前提にすることを明記しています。
Drizzleの雛形が、手元から本番のDBに書き込める設定だった
sv が作ったDrizzleの設定は、D1のAPIトークンを使って、Cloudflare上のD1に手元から直接書き込むものでした。
// 雛形の drizzle.config.ts(抜粋)
export default defineConfig({
dialect: 'sqlite',
driver: 'd1-http',
dbCredentials: {
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
databaseId: process.env.CLOUDFLARE_DATABASE_ID,
token: process.env.CLOUDFLARE_D1_TOKEN
}
});
これだと、手元の操作ひとつで本番のデータベースを変えられてしまいます。
方針は「本番へは、PRのマージからCIでだけ反映する」なので、DrizzleはマイグレーションのSQLを生成するだけに使い、適用は wrangler で行う形に変えました。
// 変更後の drizzle.config.ts
export default defineConfig({
schema: './src/lib/server/db/schema.ts',
out: './drizzle',
dialect: 'sqlite'
});
- ローカル:
npm run db:generateでSQLを作り、wrangler d1 migrations apply DB --localで適用 - 開発・本番:CIが、デプロイの前にその環境のD1へ適用する
wrangler types --check が、ビルドの前後で結果が変わった
雛形では、型チェックとビルドの前に wrangler types --check(Workersの型ファイルが最新かの確認)が走るようになっていました。
ところが、ビルドした後だけこの確認が失敗し、E2Eテストが動きませんでした。
調べてみると、ビルドの成果物があるかどうかで、生成される型ファイルの中身が変わっていました。ファイルをコミットしておくと、ビルドの前後どちらかで必ず「古い」と判定されてしまいます。
型ファイルはコミットせず、npm install・型チェック・ビルドのたびに作り直す形にしました。
ビルド後の型チェックで、400件以上のエラー
さらに、ビルドした後に型チェックを走らせると、ビルドの成果物(.svelte-kit/ の中のJavaScript)に400件以上の型エラーが出ました。
雛形の設定がJavaScriptの型チェック(checkJs)を有効にしていたためです。JavaScriptは手で書かず、TypeScriptだけを使うので、checkJs を外しました。
Prettierが CLAUDE.md の表を崩した
最後に、コードの整形(npm run format)をかけたら、CLAUDE.md の表がきれいに幅揃えされました。
見た目は整っていますが、1行がとても長くなり、手で編集しにくくなります。Markdownは整形の対象から外しました。
3. PRごとの自動テスト(CI)
GitHub Actionsで、develop と main 向けのPRごとに、次の順でテストを走らせます。
npm ci → 型チェック → Lint → Vitest → Playwright
Playwright(E2Eテスト)は、CIの中でビルド・起動したアプリに向けて実行します。Cloudflare Accessで保護した開発環境に向けると、ログインを突破する仕組みが必要になるためです。
main のブランチ保護が使えなかった
main には、「テストが通ったPRしかマージできない」ブランチ保護をかけるつもりでした。
ところが設定しようとすると、**「GitHub Proにするか、リポジトリを公開すれば使える」**と返されました。無料プランの非公開リポジトリでは使えない機能だったのです。
有料プランにする、リポジトリを公開する、という選択肢も考えましたが、今は1人の開発です。当面は保護なしで進め、「main と develop に直接pushしない」をリポジトリの CLAUDE.md にルールとして書いて、Claude Codeにも守らせることにしました。
本番へのデプロイでもテストを走らせ、失敗したらデプロイしないようにしているので、安全の大部分は確保できていると考えています。
E2Eテストの置き場所
雛形は、E2Eテストのファイルを src/routes/(ページのソースと同じ場所)に置いていました。
レビューのときに気になって、置き場所を変えてもらいました。
- E2Eは、「プランを選ぶ → チェックリストを作る → コンテナを一括チェックする」のように、ページをまたぐ流れを確かめるもの。特定のページの隣に置く理由がない
- ユニットテスト(Vitest)は、1つの関数を確かめるものなので、ソースの隣に置いたままにする
移動先は、ルートの e2e/ ではなく tests/e2e/ にしました。SvelteKitが自動で作るTypeScriptの設定が、tests/ の下を最初から型チェックの対象に含めているためです。
わざと失敗させてみたら、足りないものが見つかった
CIが本当に失敗を検知するか確かめるため、トップページの見出しを「ハコヅミ」から**「ハコヅメ」**にわざと変えてpushしてみました。
狙いどおり、E2Eテストで失敗し、CIは赤になりました。
Expected: "ハコヅミ"
Received: "ハコヅメ"
ところが、失敗したときに残すように設定していた結果ファイルを見ると、スクリーンショットが入っていませんでした。Playwrightは、設定しないと失敗時のスクリーンショットを撮らないのです。
// playwright.config.ts に追加
use: {
screenshot: 'only-on-failure',
trace: 'retain-on-failure'
}
「失敗したら画面を見られる」つもりでいたので、一度わざと壊してみた甲斐がありました。
4. developは開発用、mainは本番用アカウントへの自動デプロイ
技術スタックの記事で決めた環境構成を、実際に組みました。
| ブランチ | GitHubの環境 | デプロイ先 |
|---|---|---|
develop |
development |
開発用のCloudflareアカウント |
main |
production |
本番用のCloudflareアカウント |
GitHubの「環境」で、アカウントを切り替える
GitHubのリポジトリの設定には「Environments(環境)」という機能があります。環境ごとに、別々のSecrets(秘密の値)を持たせられます。
development と production のどちらにも、同じ名前(CLOUDFLARE_API_TOKEN、CLOUDFLARE_ACCOUNT_ID)で、それぞれのアカウント用の値を登録しました。
デプロイのワークフローでは、pushされたブランチで環境を選ぶだけです。
jobs:
test:
uses: ./.github/workflows/ci.yml # PRと同じテストを呼び出す
deploy:
needs: test # テストが通らないとデプロイしない
environment: ${{ github.ref_name == 'main' && 'production' || 'development' }}
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
WRANGLER_ENV_ARGS: ${{ github.ref_name == 'main' && '--env production' || '' }}
steps:
# (checkout・setup-node・npm ci は省略)
- run: npm run build
- name: D1 マイグレーション
run: |
if ls drizzle/*.sql >/dev/null 2>&1; then
npx wrangler d1 migrations apply DB --remote $WRANGLER_ENV_ARGS
else
echo "マイグレーションはまだありません"
fi
- run: npx wrangler deploy $WRANGLER_ENV_ARGS
マイグレーションがまだ1つもないと wrangler d1 migrations apply がエラーになるので、SQLがあるときだけ実行するようにしています。
非公開リポジトリでは使えない機能が多い中、環境と環境ごとのSecretsは、無料プランでも使えました。
APIトークンは、1つのアカウントだけに絞る
CloudflareのAPIトークンは、テンプレート「Edit Cloudflare Workers」に「D1 → Edit」を足して作りました。 大事なのは、「Account Resources」を、そのトークンを使うアカウントだけに絞ることです。こうしておけば、開発用のトークンで本番のアカウントを操作することはできません。
GitHubへの登録は、値をコマンドに直接書かず、入力欄に貼り付ける形にしました。コマンドの履歴にトークンが残りません。
gh secret set CLOUDFLARE_API_TOKEN --env development
手元のwranglerは、開発用アカウントにだけログインしておく
開発用のD1は、手元の wrangler d1 create で作りました。
一方、本番用のD1は、あえてCloudflareのダッシュボードで作りました。
手元の wrangler が本番用アカウントにログインしていると、うっかり実行したコマンドが本番に反映される危険があります。本番用のD1を作るのは最初の1回だけなので、手作業でも手間はほとんど変わりません。
「手元から本番に触らない」を、心がけではなく仕組みで守るための選択です。
wrangler.jsonc の落とし穴2つ
本番用の設定は、wrangler.jsonc の env.production に書き分けます。ここで2つ、知らないとはまる点がありました。
{
"name": "hakozumi",
"d1_databases": [{ "binding": "DB", "database_name": "hakozumi", "database_id": "(開発用のID)" }],
"env": {
"production": {
// 指定しないと Worker 名が hakozumi-production になる
"name": "hakozumi",
// バインディングは env に引き継がれないので、もう一度書く
"d1_databases": [{ "binding": "DB", "database_name": "hakozumi", "database_id": "(本番用のID)" }]
}
}
}
env.productionにnameを書かないと、Worker名が自動でhakozumi-productionになる- D1などのバインディングは
envに引き継がれないので、両方に書く必要がある
初めてのデプロイ
develop にマージすると、テスト → ビルド → デプロイが自動で走り、開発用アカウントに hakozumi のWorkerができました。
続けて develop → main のPRをマージすると、本番用アカウントにも同じようにデプロイされました。
develop → main のPRは、「Squash and merge」ではなく**「Create a merge commit」**でマージしています。Squashにすると main 側に別のコミットができ、develop と main の履歴がずれてしまうためです。
5. Cloudflare Accessで、自分だけが見られるようにする
workers.dev のURLは、そのままだとURLを知っている人なら誰でも開けます。開発中のアプリを、開発用・本番用とも自分だけが見られるようにするため、Cloudflare Accessをかけました。Accessを使うのは初めてです。
設定した場所
事前に調べた手順(Settings → Domains & Routes)と、実際の日本語の画面は少し違っていました。 Workerの画面の「ドメイン」タブに、URLごとに「Accessを有効化」というリンクがありました。
設定したのは2つだけです。
- スコープ:「すべてのトラフィック」。初期値の「プレビューのみ」だと、確認用のURLしか守られず、本体のURLは誰でも開けるままになる
- 認証ポリシー:事前設定済みの「Cloudflareアカウント」。そのCloudflareアカウントのメンバーだけが入れる
ポリシーには「メールドメイン」という選択肢もありましたが、これはドメイン全体を許可する方式です。gmail.com を指定すると、Gmailの利用者なら誰でも入れてしまいます。
気をつけたいのは、画面の右側にある青いスイッチです。これはURLそのもののオン・オフで、Accessとは別物です。また、URLに付いている「プロダクション」という表示は、Cloudflareの用語で「そのWorkerの本番のURL」という意味で、開発用アカウントでもそう表示されます。
ボタン1つで入れた理由
設定後にURLを開くと、Accessのログイン画面に転送されるようになりました。 メールアドレスを入力してコードを受け取る流れを想像していたのですが、実際には「Cloudflare」のボタンを押すだけで、そのままページが表示されました。
選んだポリシーが、Cloudflareへのログインで本人確認をする方式だったためです。ダッシュボードにログインしたままのブラウザだったので、ボタン1つで「アカウントのメンバー」と判断されました。 ダッシュボードにログインしていないブラウザで開くと、まずCloudflareへのログインを求められます。
なお、本番用のWorkerは、一度デプロイしてWorkerができてからでないとAccessをかけられません。最初のデプロイからAccessを設定するまでの数分間は、誰でも開ける状態でした(中身は見出し1つだけです)。
Claude Codeとの分担を振り返って
今回は、IssueとPRの作成、実装、テスト、手元での確認はClaude Codeが行い、自分はレビューと判断を担当しました。 振り返ると、自分が判断したのは次のような点でした。
- 事業用のアカウントに作るか、コミットの作者情報をどうするか
- Node.jsを更新するか(ほかのプロジェクトにも影響するので、自分で更新した)
- ブランチ保護が使えないとき、どうするか(有料プランにするか、ルールで守るか)
- E2Eテストの置き場所(レビューで気になって変えてもらった)
- CIが本当に落ちるか、わざと壊して確かめるか
- 本番用のD1を、どこで作るか
APIトークンの作成と登録、Accessの設定は、Cloudflareのダッシュボードで自分で行いました。トークンの値をClaude Codeに渡さずに済むよう、手順だけを教えてもらう形です。
開発プロセスの記事で「何を作るか・コードの理解・受入判断は、自分で持つ」と書きました。土台づくりでも、この分担はうまく回ったと感じています。
おわりに
これで、開発の土台がそろいました。
- PRを作ると、テストが自動で走る
developにマージすると開発環境に、mainにマージすると本番環境に、自動でデプロイされる- 開発環境も本番環境も、自分しか見られない
ひとつだけ、まだ確かめられていないことがあります。マイグレーションが、デプロイ先と同じ環境のD1に適用されることです。マイグレーションがまだ1つもないためです。 次回は、最初のテーブル(ギア)を作り、ギアの登録画面を作りながら、この点も確かめます。