0. この手順書について
Cloudflare 上に社内申請システムを新規設置する手順です。上から順に実行すれば 完了します。途中で判断を求められる箇所には「★判断」と書いてあります。
作業を始める前に、次の 3 点を確認してください。
- Cloudflare アカウントの管理者権限があること
- Workers 有料プラン(月 $5)の契約、または契約できる決裁が取れていること
- Docker Desktop がインストールされた作業用 PC があること
なぜ有料プランが必要か 無料プランは 1 日 10 万リクエストかつ 1 リクエストあたり CPU 時間 10ms の制限が あり、50 名規模の社内システムでも上限に触れます。月 $5 で全サービスの 制限が実用水準まで緩和されるため、ここは節約する箇所ではありません。
設置の全体像
| 手順 | 内容 | 目安 | 自動化 |
|---|---|---|---|
| 1 | 事前準備(API トークン発行) | 30分 | 手動 |
| 2 | 開発コンテナの起動 | 30分 | ほぼ自動 |
| 3 | Cloudflare リソースの一括作成 | 5分 | 全自動 |
| 4 | Cloudflare Access の設定 | 30分 | 手動 |
| 5 | 初回デプロイ | 15分 | ほぼ自動 |
| 6 | GitHub 自動デプロイの設定 | 30分 | 手動 |
| 7 | 初期データの登録 | 1〜2時間 | 手動 |
| 8 | 設置後の確認 | 30分 | 手動 |
1. 事前準備
1-1. Account ID を確認する
Cloudflare ダッシュボードにログインし、右下または任意のドメイン概要ページに 表示される 32 桁の Account ID を控えます。
1-2. API トークンを発行する
マイプロフィール → API トークン → トークンを作成 → カスタムトークンを作成
★判断: 権限は必要最小限にします。以下を過不足なく設定してください。
| 種別 | リソース | 権限 |
|---|---|---|
| アカウント | Workers スクリプト | 編集 |
| アカウント | Workers KV ストレージ | 編集 |
| アカウント | Workers R2 ストレージ | 編集 |
| アカウント | D1 | 編集 |
| アカウント | Vectorize | 編集 |
| アカウント | Workers AI | 読み取り |
| アカウント | Secrets Store | 編集 |
| アカウント | アカウント設定 | 読み取り |
重要: 表示されたトークン文字列はこの画面でしか見られません。 パスワード管理ツールに保存してください。紛失した場合は再発行になります。 また、このトークンは本番環境を破壊できる権限を持ちます。Slack やメールで 平文で送らないでください。
1-3. リポジトリを配置して .env を作る
unzip cf-internal-app-starter.zip
cd cf-internal-app-starter
cp .env.example .env
cp .dev.vars.example .dev.vars
.env を開いて、1-1 と 1-2 で取得した値を記入します。
CLOUDFLARE_API_TOKEN=(1-2 で発行したトークン)
CLOUDFLARE_ACCOUNT_ID=(1-1 で確認した 32 桁)
ANTHROPIC_API_KEY=(Claude で開発する場合のみ。空でも可)
.envと.dev.varsは.gitignore済みです。Git にコミットされません。
2. 開発コンテナの起動
docker compose up -d
docker compose exec dev bash
以降のコマンドはすべてコンテナの中で実行します。
コンテナには Node 22 / Wrangler / Claude Code / jq / sqlite3 が入っています。 作業者の PC に Node をインストールする必要はありません。
2-1. ローカルで動作確認する
本番に触る前に、手元で動くことを確認します。
npm run db:migrate:local # ローカル DB にテーブルを作る
npm run db:seed:local # テストデータを入れる
npm run dev
ブラウザで http://localhost:5173 を開き、申請一覧が表示されれば成功です。
ローカルでは
DEV_BYPASS_AUTH=trueにより認証が迂回され、開発用ユーザーで ログインした状態になります。この設定はAPP_ENV=productionでは無効化される よう二重に条件が入っており、本番で認証がすり抜けることはありません (自動テストで検証済み)。
Ctrl+C で停止します。
3. Cloudflare リソースの一括作成
ここがこの手順書で最も自動化されている部分です。1 コマンドで完了します。
npm run bootstrap
実行されること:
- D1 データベース
synon-internal-dbを作成 → ID を取得 - R2 バケット
synon-internal-filesを作成 - KV ネームスペース
synon-internal-cacheを作成 → ID を取得 - Vectorize インデックス
synon-internal-docs(768次元 / cosine)を作成 - Vectorize のメタデータインデックス(category)を作成
- Secrets Store
synon-internal-secretsを作成 → ID を取得 - 取得した ID を
wrangler.jsoncに自動で書き戻す - 本番 D1 にテーブルを作成(マイグレーション適用)
このスクリプトは冪等です。既に存在するリソースはスキップして ID の取得だけ 行うため、途中で失敗しても安心して再実行できます。
3-1. うまくいかないとき
| 症状 | 原因と対処 |
|---|---|
CLOUDFLARE_API_TOKEN が未設定です |
.env を作った後にコンテナを起動し直す(docker compose down && docker compose up -d) |
D1 の ID を取得できませんでした |
トークンに D1 の編集権限が無い。1-2 の表を再確認 |
Secrets Store の ID を自動取得できませんでした |
ダッシュボードの Secrets Store 画面で ID を確認し、wrangler.jsonc の <SECRETS_STORE_ID> を手で置き換える(通知機能を使わないなら後回しで可) |
3-2. ステージング環境も作る場合
★判断: 本番と分けた検証環境が必要なら、続けて実行します。50 名規模で 変更頻度が低い場合は、本番のみで運用しても構いません。
ENV_SUFFIX=-staging npm run bootstrap
4. Cloudflare Access の設定
この手順を飛ばすと、システムが社外に公開されたままになります。必ず実施してください。
進行中の AWS Client VPN 移行案件と同じ ID 基盤に載せるため、認証方式は 既存方針(会社メールへのワンタイム PIN + WARP 端末限定)に揃えます。
4-1. アプリケーションを作成する
Zero Trust → Access → Applications → Add an application → Self-hosted
| 項目 | 設定値 |
|---|---|
| Application name | 社内申請システム |
| Session Duration | 24 時間 |
| Application domain | この Worker のルート(synon-internal-app.<サブドメイン>.workers.dev または独自ルート) |
4-2. ポリシーを設定する
| 項目 | 設定値 |
|---|---|
| Policy name | 社員のみ |
| Action | Allow |
| Include | Emails ending in → @synon.co.jp |
★判断: 部署単位でアクセスを絞りたい場合は、ここで Emails に個別の
アドレスを列挙するか、将来 IdP 連携したうえでグループ条件を使います。
初期導入では全社員許可(上記)で始め、必要が出てから絞るのが実務的です。
4-3. AUD タグを設定ファイルに貼る
アプリケーション作成後、Overview タブに表示される
Application Audience (AUD) Tag(64 桁の16進文字列)をコピーします。
wrangler.jsonc を開き、<ACCESS_AUD_TAG> を置き換えます。
"vars": {
"ACCESS_TEAM_DOMAIN": "https://synon.cloudflareaccess.com",
"ACCESS_AUD": "3f7a9c...(ここに貼る)",
"DEV_BYPASS_AUTH": "false",
"APP_ENV": "production"
},
ACCESS_TEAM_DOMAIN も自社のチームドメインに合わせてください
(Zero Trust → Settings → Custom Pages などで確認できます)。
なぜ Worker 側にも AUD が要るのか Access をルートに紐付けても、
workers.devのアドレスを直接叩かれる経路が 残り得ます。そのため Worker 側でも JWT の署名・発行元・宛先(AUD)を 再検証しています。AUD タグが未設定・誤りだと全ユーザーが 401 になります。
4-4. 通知用の Webhook を登録する(任意)
Slack 通知を使う場合のみ。
npx wrangler secrets-store secret create <STORE_ID> \
--name SLACK_WEBHOOK_URL --scopes workers --remote
対話的に値の入力を求められるので、Slack の Incoming Webhook URL を貼ります。
Secrets Store に置いた値は、再デプロイなしで更新できます。Webhook を 差し替えるときはこのコマンドを実行するだけです。
5. 初回デプロイ
npm run deploy
フロントエンドをビルドし、Worker と一緒に配信します。完了すると URL が 表示されます。
5-1. 動作確認
# 認証不要の死活監視エンドポイント
curl https://<デプロイされたURL>/api/health
# → {"ok":true,"env":"production","time":"..."}
続いてブラウザで URL を開きます。Cloudflare Access のログイン画面が出て、 会社メールに届いた PIN を入力すると申請一覧が表示されれば設置成功です。
5-2. うまくいかないとき
| 症状 | 原因と対処 |
|---|---|
| ログイン画面が出ずに直接画面が表示される | Access アプリのドメイン指定が Worker の URL と一致していない |
| ログイン後に画面が真っ白/401 が出る | ACCESS_AUD または ACCESS_TEAM_DOMAIN の設定誤り。4-3 を再確認 |
| 「規程検索」だけ 503 エラー | Vectorize / Workers AI の設定。他の機能には影響しないので後回しで可 |
6. GitHub 自動デプロイの設定
以降の改修を git push だけで反映できるようにします。
6-1. リポジトリに登録するもの
Settings → Secrets and variables → Actions
Secrets(秘匿値)
| 名前 | 値 |
|---|---|
CLOUDFLARE_API_TOKEN |
1-2 で発行したトークン |
CLOUDFLARE_ACCOUNT_ID |
1-1 で確認した ID |
Variables(非秘匿値)
| 名前 | 値 |
|---|---|
PRODUCTION_URL |
https://<本番URL>(スモークテスト用。未設定でも動作します) |
6-2. 本番デプロイに承認を挟む
★判断: ここは省略できますが、強く推奨します。
Settings → Environments → New environment → 名前を production
→ Required reviewers に情シス担当者を追加
これにより、main への push は自動検証まで進んだあと人間の承認待ちで停止
します。承認して初めて本番に出ます。
6-3. パイプラインの動き
| ブランチ | 動作 |
|---|---|
| プルリクエスト | 型チェック・テスト・ビルド検証のみ。本番には出ない |
staging |
検証 → ステージング環境へ自動反映 |
main |
検証 → 人間の承認 → DB移行 → デプロイ → スモークテスト → 失敗時は自動ロールバック |
注意: 自動ロールバックが戻すのはアプリケーションのコードだけです。 データベースのスキーマ変更は自動では戻りません。テーブル定義を変える改修は、 必ずステージングで先に試してください。
7. 初期データの登録
7-1. 利用者を登録する
利用者は事前登録が不要です。 Access で認証が通った人が初めてアクセスした 時点で、自動的に一般利用者として登録されます。
登録が必要なのは権限を持つ人だけです。
npx wrangler d1 execute DB --remote --env="" --command "
INSERT INTO users (email, display_name, department, role) VALUES
('mtanaka@synon.co.jp', '田中 実', '情報システム部', 'admin'),
('(部門長のアドレス)', '(氏名)', '(部署)', 'approver')
ON CONFLICT(email) DO UPDATE SET role = excluded.role;
"
| role | できること |
|---|---|
member |
申請の作成・提出・自分の申請の閲覧(既定値) |
approver |
上記 + 自分に割り当てられた申請の決裁 |
admin |
上記 + 設定変更・全添付ファイルの閲覧 |
7-2. 承認ルートを設定する
金額に応じて承認者を自動で割り当てる設定です。この設定は画面から変更でき、 システム改修は不要です。
curl -X PUT https://<本番URL>/api/settings/approval-rules \
-H "Content-Type: application/json" \
-H "CF-Access-Client-Id: ..." \
-d '[
{"threshold": 100000, "approver": "(10万円以上の承認者)"},
{"threshold": 0, "approver": "(10万円未満の承認者)"}
]'
threshold は「この金額以上ならこの人」という下限値です。降順に評価されます。
7-3. 社内規程を登録する
規程検索で引けるようにする文書を登録します。1 件ずつ API で投入します。
curl -X POST https://<本番URL>/api/search/documents \
-H "Content-Type: application/json" \
-d '{
"id": "doc-expense-rule",
"title": "旅費精算規程",
"content": "(規程の本文をここに)",
"category": "rule"
}'
★判断: 長い規程は章ごとに分割して登録すると検索精度が上がります。ただし 分割数だけ Vectorize の使用量が増えます(無料枠は約 1 万 3 千件相当)。 まずは規程 1 本 = 1 件で登録し、精度が足りなければ分割してください。
8. 設置後の確認
以下をすべてチェックして完了です。
機能確認
-
/api/healthが 200 を返す - Access のログイン画面が表示される
- 会社メール以外のアドレスでログインできない
- 申請を作成 → 提出 → 別アカウントで承認、まで一通り動く
- 添付ファイルをアップロードし、ダウンロードできる
- 申請と無関係な第三者が添付をダウンロードできない
- 規程検索で結果が返る
- スマートフォンのブラウザでも画面が崩れない
運用準備
- 退職者の Access シート回収を月次業務に組み込んだ
- 障害時の連絡先と対応手順を関係者に共有した
- API トークンをパスワード管理ツールに保管した
- 運用(操作)説明書を利用者に配布した
最重要: Zero Trust Free プランは 50 ユーザーまでです。シートは認証イベントで 消費され、手動で削除するまで保持されます。退職者の回収を運用に組み込まないと、 実在の人数より早く上限に達し、51 人目のログインがブロックされます。 これは進行中の VPN 移行案件と共通の論点です。
付録 A. よく使うコマンド
すべてコンテナ内(docker compose exec dev bash)で実行します。
npm run dev # ローカル開発サーバー起動
npm run typecheck # 型チェック
npm test # 自動テスト
npm run deploy # 本番へ手動デプロイ
npm run deploy:staging # ステージングへデプロイ
npm run tail # 本番ログをリアルタイム表示
npm run rollback # 前バージョンへ戻す
# 本番 DB に直接クエリを投げる(参照は安全、更新は慎重に)
npx wrangler d1 execute DB --remote --env="" --command "SELECT COUNT(*) FROM requests"
付録 B. 設置作業の記録欄
| 項目 | 値 |
|---|---|
| 設置実施日 | |
| 実施者 | |
| Cloudflare Account ID | |
| 本番 URL | |
| Access チームドメイン | |
| Access AUD タグ | |
| API トークンの保管場所 | |
| GitHub リポジトリ URL | |
| 本番デプロイ承認者 |