| 項目 | 内容 |
|---|---|
| ドキュメント版数 | v0.3 |
| 作成日 | 2026-08-09 |
| 更新日 | 2026-08-11 |
| 主な変更 | 自動デプロイの記述を実態に修正。 v0.1 は手動実行のみで、push トリガーは未有効 |
| 参照 | ZIP ファイルデプロイ / enebular CLI / GitHub Actions 自動化 |
| コンポーネント | デプロイ先 | 方法 |
|---|---|---|
apps/function + apps/web |
enebular クラウド実行環境(ZIP) | GitHub Actions → @uhuru/enebular-cli |
| データストア | enebular データストア | コンソールで手動作成(テーブル ID を envVars へ) |
フロントエンドは ZIP に同梱される。デプロイ先は 1 つだけ (ADR-012)。 別ホスティングを使わないため、バージョンずれも CORS 設定も発生しない。
enebular プロジェクトは 1 つ。その中にクラウド実行環境を 2 つ持つ(F19 Won't)。
プロジェクトを分けないので ENEBULAR_PROJECT_ID は共通で、
実行環境とファイルアセットだけが環境ごとに異なる。
| GitHub Environment | 用途 | トリガー URL | 状態 |
|---|---|---|---|
staging |
通しの動作確認・デモ | .../socrametry-stg |
設定済み |
development |
開発中の動作確認 | .../socrametry |
設定済み |
production |
本番 | — | 未作成(v0.2。Required reviewers を付けて承認制にする) |
productionを選ぶとワークフローは設定不足で止まる。 Environment が無いため Secrets も Variables も空になり、 「Validate credentials and settings」で明示的に失敗する。 ZIP のデプロイまで進んでから壊れることはない。
| ブランチ / イベント | デプロイ先 | 版 | 状態 |
|---|---|---|---|
手動実行 (workflow_dispatch) |
選択した Environment(既定 staging) |
v0.1 | ✅ これだけが有効 |
develop への push |
staging | v0.2 以降 | ⬜ ワークフロー内でコメントアウト |
v* タグの push |
production | v0.2 以降 | ⬜ 同上 |
v0.1 は手動実行のみに絞っている。 フロントエンドが暫定のため、 意図しないタイミングでデモ環境が入れ替わるのを避ける。 正式なフロントエンドが載って 1 画面が通った時点で push トリガーを有効化する (deploy-function.yml のコメントを外すだけ)。
公式ドキュメントより、 Node.js の ZIP は次を満たす必要がある。
| # | 要件 |
|---|---|
| 1 | ZIP のルート直下に index.js と package.json を置く(親フォルダで包まない) |
| 2 | ハンドラは index.handler。exports.handler = async (event) => {...} の CommonJS 形式 |
| 3 | package.json に "type": "module" を書かない(ES Modules と非互換) |
| 4 | 戻り値は { statusCode, headers, body }。body は文字列(JSON は JSON.stringify) |
| 5 | ZIP サイズは 250MB 以下 |
| 6 | 外部パッケージを使う場合は node_modules を同梱する |
pnpm ワークスペースの node_modules は symlink 構造のため、
そのまま ZIP に入れるとワークスペース依存(@socrametry/core 等)が壊れる。
そこで esbuild で単一 CommonJS ファイルにバンドルする
(ADR-008)。
apps/function/
├── src/index.ts → esbuild → build/index.js (CJS / 依存を内包)
├── zip-package.json → コピー → build/package.json
└── build.mjs build/ → socrametry-function.zip
apps/web/
├── src/**.js → esbuild → public/app.js (ADR-013)
└── public/
├── index.html ┐
├── styles.css ├→ esbuild が文字列として index.js に取り込む (ADR-012)
└── app.js ┘
build.mjsは最初にapps/webをビルドする。public/app.jsは生成物で git 管理していないため、ここで作らずに読むと 古い(または存在しない)ファイルが ZIP に入る。CI もデプロイもpnpm --filter @socrametry/function build:zipを直接呼ぶ(turbo を経由しない)ので、 依存関係ではなくスクリプトの中から明示的に呼んでいる。
静的ファイルは別途 ZIP に入れず、バンドルに文字列として含める。 Lambda のファイルシステム読み込みが不要になり、 ZIP の中身は
index.jsとpackage.jsonの 2 つだけで済む。
apps/function/build.mjs(実装は apps/function/build.mjs。以下は骨子)
// loader オプションで .html / .css / .js を文字列として取り込む(ADR-012)
// loader: { '.html': 'text', '.css': 'text' }
import { build } from 'esbuild'
import { cp, rm, mkdir } from 'node:fs/promises'
import { createWriteStream } from 'node:fs'
import archiver from 'archiver'
const OUT = 'build'
await rm(OUT, { recursive: true, force: true })
await mkdir(OUT, { recursive: true })
// 1) 単一 CJS にバンドル(ワークスペース依存もすべて内包される)
await build({
entryPoints: ['src/index.ts'],
outfile: `${OUT}/index.js`,
bundle: true,
platform: 'node',
target: 'node22',
format: 'cjs', // ★ enebular の要件(要件 2)
minify: true,
sourcemap: false,
})
// 2) ZIP 用の最小 package.json("type": "module" を含まないもの)
await cp('zip-package.json', `${OUT}/package.json`)
// 3) build/ の "中身" を ZIP のルートに詰める(親フォルダで包まない: 要件 1)
const zip = archiver('zip', { zlib: { level: 9 } })
zip.pipe(createWriteStream('socrametry-function.zip'))
zip.directory(`${OUT}/`, false) // ★ false がルート直下配置の指定
await zip.finalize()apps/function/zip-package.json
{
"name": "socrametry-function",
"version": "0.1.0",
"description": "SocraMetry backend on enebular cloud execution environment",
"main": "index.js"
}
"type": "module"を書かないこと。ビルドスクリプトがこのファイルをコピーするだけの 運用にしておけば、apps/function/package.json(こちらは"type": "module")の 設定に引きずられて壊れることがない。build.mjsは起動時にこのファイルを読み、type: "module"が入っていたらビルドを失敗させる。
apps/function/src/index.ts
import { handle } from 'hono/aws-lambda'
import { app } from './app'
// enebular のハンドラ指定は index.handler
export const handler = handle(app)デプロイ前に、同じ app を Lambda なしで起動して動作確認する(NFR-Q3)。
pnpm --filter @socrametry/function dev # apps/function/src/local.ts を起動ZIP の中身の検証も CI で行う。
unzip -l socrametry-function.zip | head # index.js と package.json がルートにあるかハンドラの検証は文字列検索で行わない。 esbuild の CommonJS 出力は
__toCommonJSを経由してmodule.exportsを組み立てるため、 バンドル結果にexports.handlerという字面は現れない。require()してtypeof handler === 'function'を確認する (契約そのものを検証する)方式にしている。build.mjsと CI の両方で実施する。
CLI は既存のアセットと実行環境に対して動くため、最初の 1 回はコンソールで作る。
| # | 作業 | 取得する ID |
|---|---|---|
| 1 | プロジェクトを 1 つ作成 ※ production は v0.2(F19 Won't) | PROJECT_ID |
| 2 | データストアのテーブルを 5 つ作成(§3.1) | テーブル ID × 5 |
| 3 | ZIP をファイルアセットとして登録(--deploy-type cloud --handler index.handler) |
ASSET_ID |
| 4 | ZIP 向けクラウド実行環境を作成(ランタイム Node.js 22.x) | CLOUD_ID |
| 5 | HTTP トリガーを有効化しパスを設定(インスタンス内で一意) | トリガー URL |
3〜5 は環境の数だけ繰り返す。 staging と development で ファイルアセットとクラウド実行環境を分けているため(プロジェクトは共通)。 トリガーのパスも変える(
socrametry/socrametry-stg)。 フロントは相対パスで API を呼ぶので(ADR-012)、パスが違っても画面側の設定は要らない。 | 6 |connectDataStoreを有効化し、環境変数を設定 | — | | 7 | アクセスキー / シークレットキーを発行 |ENEBULAR_ACCESS_KEY/ENEBULAR_SECRET_KEY|
手順 3 は ZIP の実体を要求する。 アプリの実装前にセットアップを進める場合は、
exports.handlerが 200 を返すだけの最小 ZIP を登録しておき、 以降は CI のenebular update fileで中身を差し替える。
コンソールでの作成時にキー名と型を指定する。定義は data-model.md §2 と scope-v0.1.md §4.4 が正。
| # | テーブル | メインキー | サブキー | サブキー型 | 対応する環境変数 |
|---|---|---|---|---|---|
| 1 | users |
email |
kind(値は "account" 固定) |
文字列 | DS_TABLE_USERS |
| 2 | sessions |
ownerId |
sessionId(ULID) |
文字列 | DS_TABLE_SESSIONS |
| 3 | session_secrets |
sessionId |
kind |
文字列 | DS_TABLE_SECRETS |
| 4 | reports |
ownerId |
sessionId(ULID) |
文字列 | DS_TABLE_REPORTS |
| 5 | ops_logs |
sessionId |
ts(epoch ms) |
数値 | DS_TABLE_OPS_LOGS |
org_directory / member_stats / assignments / question_bank は v0.2 で追加する(F16 Won't)。
v0.1 はフリー / トライアル枠を前提とするため、テーブル数上限 10 に対して 5 で収める(前提 A-8)。
ops_logsのサブキーだけが数値である点に注意。 文字列で作ると時系列の範囲クエリ(A5)が辞書順になり、桁が変わった時点で壊れる。
enebular add file \
--project-id <プロジェクトID> \
--file ./socrametry-function.zip \
--deploy-type cloud \
--handler index.handler \
--name "SocraMetry Function" \
--detail "SocraMetry backend"enebular bulk-update cloud-config の設定ファイルで指定できるキーは
name / httpTriggerStatus / httpTriggerPath / scheduleTriggerStatus /
scheduleTriggerDef / timeout / connectAgent / connectDataStore / envVars。
enebular bulk-update cloud-config \
--project-id <プロジェクトID> \
--cloud-id <クラウド実行環境ID> \
--config-file ./config.json方針(未決 Q-8): envVars には ORCAROUTER_API_KEY が含まれるため、
設定ファイルをリポジトリにコミットしない。v1 は以下を推奨する。
| 案 | 内容 | 採否 |
|---|---|---|
| A(推奨) | envVars はコンソールで手動管理し、CI は ZIP の更新とデプロイのみ行う |
v1 で採用 |
| B | CI で GitHub Secrets からテンプレートを埋めて config.json を生成し bulk-update する |
キーのローテーションを自動化したくなったら移行 |
案 A なら、CI が知る必要のあるシークレットは enebular のアクセスキーだけになり、 OrcaRouter のキーが GitHub 側に一切存在しない状態を保てる。
実装は .github/workflows/deploy-function.yml。
| Secret | 用途 |
|---|---|
ENEBULAR_ACCESS_KEY |
enebular CLI の認証 |
ENEBULAR_SECRET_KEY |
同上 |
値の前後に空白や改行が入っていると認証エラーになる(公式ブログより)。 ワークフローの冒頭で空チェックを行っている。
シークレットではない ID は Variables(vars)で管理し、環境ごとに切り替える。
| Variable | 用途 |
|---|---|
ENEBULAR_PROJECT_ID |
デプロイ先プロジェクト ID |
ENEBULAR_CLOUD_ID |
クラウド実行環境 ID |
ENEBULAR_FILE_ASSET_ID |
ZIP のファイルアセット ID |
GitHub Environments を作り、環境ごとにこの 3 つを設定する。
v0.1 は staging と development の 2 つ。enebular プロジェクトは 1 つなので
ENEBULAR_PROJECT_ID は両者で同じ値になり、ENEBULAR_CLOUD_ID と
ENEBULAR_FILE_ASSET_ID だけが分かれる。
production は v0.2 で追加し、Required reviewers を付けて承認制にする。
このほか、デプロイ後の疎通確認に HTTP_TRIGGER_URL(トリガー URL)を設定できる。
未設定ならスモークテストのステップはスキップされる。
build → update file → deploy cloud → add file-version
| # | ステップ | コマンド |
|---|---|---|
| 1 | ZIP をビルド | pnpm --filter @socrametry/function build:zip |
| 2 | ファイルアセットを差し替え | enebular update file --project-id … --asset-id … --file … --json |
| 3 | クラウド実行環境へデプロイ | enebular deploy cloud --project-id … --cloud-id … --asset-id … --asset-type file --json |
| 4 | バージョンを記録 | enebular add file-version --project-id … --asset-id … --name … --comment … --json |
--jsonを必ず付ける。 GitHub Actions では対話的な確認ができないため、 これがないと確認プロンプトで workflow が停止する。--yesでも同様の効果がある。
--asset-typeはfile。 ブログ記事の例は Node-RED フローのデプロイのためflowだが、 ZIP デプロイではfileを指定する。
実測(staging):
| パス | 返ってくる Cache-Control |
|---|---|
/(HTML) |
no-cache ← static.ts が付けたものが残る |
/styles.css |
max-age=14400 |
/app.js |
max-age=14400 |
実行環境の前段(Cloudflare)が拡張子で判断して上書きする。
static.ts は全ファイルに no-cache を付けているが、.css と .js では残らない。
これはデプロイの失敗より気づきにくい。
/v1/healthのcommitは新しく、サーバも新しいファイルを返しているのに、 ブラウザは 4 時間前の CSS と JS を使い続ける。 「デプロイしたのに画面が変わらない」という形で出る。 実際、この挙動に気づくまで修正を 3 回デプロイしていた。
ヘッダを通せない以上、URL を変えるしかない。
index.html はそのまま届くので、そこにコミットを差し込む。
<link rel="stylesheet" href="styles.css?v=__ASSET_VERSION__" />
<script src="app.js?v=__ASSET_VERSION__"></script>__ASSET_VERSION__ は static.ts が配信時にコミットへ置換する
(ローカルは起動ごとに変わる値)。コミットが変われば URL が変わり、
前段は別のリソースとして扱う。
確認のしかた:
# 版が入っているか(2 か所とも同じ値になる)
curl -sS "$URL/" | grep -o 'v=[0-9a-f]\{12\}'?v= を外すと、この問題がそのまま戻る。static.test.ts で検査している。
enebular add file-version でバージョンを記録しているため、
コンソールから過去バージョンを選んで再デプロイできる。
CI からのロールバックは workflow_dispatch で対象タグを指定して
ワークフローを再実行する(ビルドし直して同じ内容を再デプロイする)方式とする。
ZIP は決定的にビルドされるため、同じコミットからは同じ ZIP が得られる。
| # | 確認項目 | 検証 |
|---|---|---|
| 1 | ZIP のルート直下に index.js と package.json があるか |
自動 |
| 2 | ZIP 内 package.json に "type": "module" がないか |
自動 |
| 3 | ハンドラ指定が index.handler になっているか |
自動 |
| 4 | ZIP サイズが 250MB 以下か | 自動 |
| 5 | デプロイしたコミットが実際に動いているか | 自動 |
| 6 | envVars に必要なテーブル ID と鍵が設定されているか |
自動 |
| 7 | SESSION_JWT_SECRET と INVITE_CODE が既定値(change-me)のままでないか |
自動 |
| 8 | connectDataStore が有効か |
手動 |
| 9 | OPS_LOG_ENABLED が true か(v0.1 はコスト実測のため有効にする / F11)→ ** GET /v1/health の limits.opsLog で確認できる。**false だと課金されるのに記録が残らず、GET /cost が MOCK と見分けられなくなる |
手動 |
| 10 | MOCK_MODE が意図した値になっているか(本番で true のまま公開しない) |
手動 |
| 11 | HTTP トリガーのパスが enebular インスタンス内で一意か | 手動 |
| 12 | timeout が API 仕様の想定レイテンシ(最大 20 秒)より長いか |
手動 |
| 13 | LOG_LEVEL が INFO 以下か(DEBUG 以上は入力内容がログに出うる) |
手動 |
自動 = デプロイのたびに CI が検証し、満たさなければワークフローが落ちる。
1〜4 は ZIP のビルド時(build.mjs)と Verify ZIP layout、
5〜7 は Smoke test が /v1/health の応答を見て判定する。
手動 = 初回セットアップ時と設定変更時に人が確認する。 9・10 は「意図した値かどうか」であり、機械には判定できない。
apps/function/src/config.ts が、動作モードに応じて必須の環境変数を判定する。
| 条件 | 追加で必須になるもの |
|---|---|
| 常に | DS_TABLE_USERS / SESSIONS / SECRETS / REPORTS、SESSION_JWT_SECRET、INVITE_CODE |
OPS_LOG_ENABLED=true |
DS_TABLE_OPS_LOGS |
MOCK_MODE が true 以外 |
ORCAROUTER_API_KEY、MODEL_DIAGNOSER / QUESTIONER / JUDGE |
.env.example の雛形の値(00000000-… / change-me)のままなら未設定として扱う。
不足しているキー名は
/v1/healthに出さない。 このエンドポイントは認証不要のため、 公開するのはconfigOk(真偽値)とconfigMissing(件数)だけ。 キー名は起動時のログ(config.incomplete)にのみ出力する。値はどこにも出さない。
起動を止める形(throw)にはしない。 設定 1 個の欠けで全リクエストが 500 になると
/v1/healthすら返らず、「関数は動いているのに何も応答しない」という 最も切り分けにくい状態になる。configOk: falseを返せる状態で立ち上がる方が診断できる。
トリガーのパスをアプリ側に設定する必要はない。 アプリは先頭セグメントを問わずルーティングするため、 トリガーのパスを変えてもアプリの再設定は要らない(ADR-009)。
10 は特に注意。
MOCK_MODE=trueのまま公開すると、 固定応答が返っていることに気づかないまま「動いている」と見えてしまう。 ここだけは自動化できないため、/v1/healthのmockModeを目視で確認する。