このブログは静的サイトジェネレータ(SSG)もバンドラも使わず、Express のサーバサイド描画だけで動いています。この記事では、その構成と設計判断を紹介します。
全体構成
- ランタイム: Node.js 24 + Express 5(依存パッケージは最小限)
- 記事: リポジトリ内の Markdown ファイル(frontmatter 付き)
- 描画: 起動時に全記事をパースして HTML 化し、メモリに保持。リクエスト時のファイル I/O はゼロ
- 配信: Cloud Run(GitHub Actions で main への push をトリガーに自動デプロイ)
なぜノービルドか
記事の追加は「Markdown を置いて push する」だけで完結させたく、ビルドパイプラインを増やしたくありませんでした。コンテンツの更新 = 再デプロイという Cloud Run の運用モデルなら、起動時に一度だけ読み込むアプローチで十分成立します。
// 起動時に content/posts/*.md を読み込んでメモリに保持する
const files = await readdir(POSTS_DIR);
const mdFiles = files.filter(f => f.endsWith('.md') && !f.startsWith('_'));
GitHub Actions によるデプロイ
main への push をトリガーに、GitHub Actions が Cloud Run へ自動デプロイします。ワークフローは単一ジョブで、やることは認証とデプロイコマンドの実行だけです。
steps:
- uses: actions/checkout@v7
- uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ vars.WIF_PROVIDER }}
service_account: ${{ vars.WIF_SERVICE_ACCOUNT }}
- uses: google-github-actions/setup-gcloud@v3
- name: Deploy to Cloud Run
run: |
gcloud run deploy "$SERVICE" \
--source=. \
--project="$GCP_PROJECT_ID" \
--region="$REGION" \
--platform=managed
ポイントは 2 つあります。
- 認証は Workload Identity 連携(OIDC)。サービスアカウントキーを GitHub Secrets に保存せず、実行時に短命トークンを取得します。長命の鍵を持たないので、漏洩やローテートの心配がありません。ワークフローが参照する
WIF_PROVIDER/WIF_SERVICE_ACCOUNTは機密ではないリソース識別子なので、Secrets ではなく GitHub Variables に置いています。トークン発行はこのリポジトリ・ブランチに紐づく連携設定と組み合わさって初めて成立するため、識別子単体では認証情報になりません - ランナー上ではビルドしない。
--sourceはソース一式を GCP へ送り、Dockerfile によるビルドを Cloud Build 側で実行します。ランナーではビルドもイメージ保持も行わず artifact も使わないため、GitHub Actions のメモリ・ディスクや artifact ストレージの枯渇とは無縁です
デプロイ後は同じジョブ内でスモークテストとして主要パス(/ /about /feed.xml /sitemap.xml)へ curl を投げ、HTTP 200 が返ることを確認しています。
コスト感
Cloud Run はリクエスト処理中のみ課金され、最小インスタンス数はデフォルトの 0 のままなので、アイドル時の料金はかかりません。個人ブログ規模のトラフィックなら月 200 万リクエスト・180,000 vCPU 秒などの無料枠に収まり、Cloud Run の利用料は実質 0 円です。
--source デプロイで生成されるコンテナイメージは Artifact Registry に蓄積しますが、クリーンアップポリシー(最新 5 世代を保持し 30 日超を自動削除)で頭打ちにしているため、無料枠 0.5 GB に収まり保存料もかかりません。実際に発生するのはドメインの維持費くらいです。代わりにアイドルからの初回アクセスにはコールドスタートが発生しますが、依存が少なく起動の軽いこの構成では許容範囲と判断しています。