はじめに
ブログをWordPressからHugo(静的サイトジェネレーター)に移行しましたが、コメント欄や掲示板のような「動的機能」が失われるのが悩みでした。 外部サービス(Disqusなど)を使う手もありますが、「せっかくエンジニアなのだから、Azureの無料枠をフル活用して自作しよう」 と思い立ち、完全サーバーレスな掲示板システムを構築しました。
今回は、バックエンドに C# (Azure Functions) 、データベースに Cosmos DB を使用した実装手順をフルスクラッチで解説します。
🛠 技術スタックとアーキテクチャ
今回のこだわりは 「完全無料(Free Tier)」 かつ 「運用が楽」 な構成です。
-
Frontend: Hugo (Azure Static Web Apps でホスティング)
-
Backend: Azure Functions (Static Web Apps の Managed Functions 機能を利用)
-
言語: C# (.NET 8 Isolated Worker)
-
Database: Azure Cosmos DB for NoSQL
-
Free Tier (月間 1000 RU/s, 25GB ストレージ無料) を適用
システム構成図
ユーザーがブラウザでアクセスすると、静的コンテンツはSWAから配信され、APIリクエスト(書き込み/読み込み)はSWA内のAzure Functionsを経由してCosmos DBにアクセスします。
システム構成図
1. データベースの構築 (Azure Cosmos DB)
まずはデータの保存先を作ります。ここでの設定が「無料」で運用するためのキモです。
- Azure Portal で 「Azure Cosmos DB for NoSQL」 を作成します。
- 容量モード: ここで必ず 「プロビジョニングされたスループット」 を選択し、「Free レベル割引の適用」を「適用」 にします。
- ※これを忘れると課金が発生するので注意!
Azure 設定画面
コンテナ(箱)の作成
リソースができたら「データエクスプローラー」からコンテナを作成します。
- Database id:
BBSDB - Share throughput: チェックを入れる (Database throughput: Manual 400 - 1000 RU/s)
- Container id:
Posts - Partition key:
/category
これでデータベースの準備は完了です。 後の工程で使うため、「キー (Keys)」 メニューから 「プライマリ接続文字列」 をコピーして控えておきます。
2. バックエンドの実装 (Azure Functions / C#)
Azure Static Web Apps (SWA) は、リポジトリ内に api というフォルダを置くだけで、自動的にバックエンドAPIとして認識してくれます。今回は VS Code と Azure Functions 拡張機能 を使って開発します。
プロジェクト作成
VS Codeのコマンドパレット (F1) から Azure Functions: Create New Project を実行します。
- フォルダ: Hugoプロジェクト直下に
apiフォルダを新規作成して選択 - 言語: C#
- ランタイム: .NET 8 Isolated
- テンプレート: HTTP trigger
- 認証レベル: Anonymous
必要なパッケージ
Cosmos DB を簡単に扱うため、NuGetパッケージを追加します。
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.CosmosDB
実装コード (BbsFunctions.cs)
Input Binding と Output Binding を使うことで、データベース接続周りのコードを一切書かずにデータベースと連携できます。
注意点: C#のプロパティは大文字(PascalCase)が標準ですが、Cosmos DB側は小文字(camelCase)で保存したいため、JsonPropertyName 属性でマッピングを行っています。これをしないとパーティションキーが見つからずエラーになります。
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Company.Function
{
public class BbsFunctions
{
private readonly ILogger<BbsFunctions> _logger;
public BbsFunctions(ILogger<BbsFunctions> logger)
{
_logger = logger;
}
// 1. 投稿データを保存するAPI (POST)
[Function("CreatePost")]
[CosmosDBOutput(
databaseName: "BBSDB",
containerName: "Posts",
Connection = "CosmosDbConnectionString", // local.settings.json または環境変数で設定
CreateIfNotExists = true,
PartitionKey = "/category")]
public async Task<object?> CreatePost(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "posts")] HttpRequest req)
{
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
var post = JsonSerializer.Deserialize<PostItem>(requestBody, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (post == null || string.IsNullOrEmpty(post.Message)) return null;
// サーバー側でIDや日時を付与
post.Id = Guid.NewGuid().ToString();
post.CreatedAt = DateTime.UtcNow;
post.Category = "general";
return post; // Output Bindingにより戻り値が自動的にDBに保存される
}
// 2. 投稿一覧を取得するAPI (GET)
[Function("GetPosts")]
public IActionResult GetPosts(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "posts")] HttpRequest req,
[CosmosDBInput(
databaseName: "BBSDB",
containerName: "Posts",
Connection = "CosmosDbConnectionString",
SqlQuery = "SELECT * FROM c ORDER BY c.createdAt DESC")] IEnumerable<PostItem> posts)
{
return new OkObjectResult(posts);
}
}
// データモデル
public class PostItem
{
[JsonPropertyName("id")]
public string Id { get; set; } = "";
[JsonPropertyName("name")]
public string Name { get; set; } = "名無し";
[JsonPropertyName("message")]
public string Message { get; set; } = "";
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("category")]
public string Category { get; set; } = "general";
}
}
3. フロントエンドの実装 (Hugo Shortcode)
記事内の好きな場所に掲示板を設置できるよう、Hugoのショートコードとして実装しました。
layouts/shortcodes/bbs.html を作成し、Vanilla JS (素のJavaScript) でAPIを叩きます。
<div id="bbs-app">
<div class="bbs-form">
<input type="text" id="name" placeholder="お名前">
<textarea id="message" placeholder="メッセージを入力..."></textarea>
<button onclick="submitPost()">書き込む</button>
</div>
<div id="posts-container"></div>
</div>
<script>
// 本番環境では相対パス、ローカル開発時はlocalhostを指定
const API_URL = "/api/posts";
async function loadPosts() {
const res = await fetch(API_URL);
const posts = await res.json();
// DOM生成処理(省略)...
}
async function submitPost() {
// POST送信処理(省略)...
}
loadPosts();
</script>
(※UIデザインやアンカー機能などの詳細は、コード量が増えるため割愛しますが、CSSで掲示板風に装飾しています)
記事Markdownファイル側では {{< bbs >}} と書くだけで掲示板が表示されます。
4. デプロイとハマりポイント(トラブルシューティング)
ローカル環境 (func host start) で動作確認後、GitHubへPushしてデプロイしましたが、ここでいくつかエラーに遭遇しました。ここが今回の開発のハイライトです。
ハマりポイント①:HTTP 500エラー (ファイアウォール)
デプロイ後、APIを叩くと 500 Internal Server Error が発生。
原因は Cosmos DBのファイアウォール でした。
初期設定では開発時の自宅IPアドレスのみ許可していたため、Azure上で動作するFunctions(Static Web Apps Managed Functions)からのアクセスが拒否されていました。
解決策:
Cosmos DBの「ネットワーク」設定で、「Azure データセンター内からの接続を許可する」 をONにする必要がありました。これをONにすると、許可リストに 0.0.0.0 が追加されます。
Azure 設定画面
ハマりポイント②:接続文字列の設定漏れ
ファイアウォールを通してもまだエラー。
原因は 環境変数の設定 です。ローカル開発用の local.settings.json はGitにコミットされないため、本番環境には鍵の情報がありません。
解決策:
Azure Portal の Static Web Apps の「構成」→「アプリケーション設定」から、環境変数 CosmosDbConnectionString を手動で追加し、先ほどコピーしたCosmos DBのプライマリ接続文字列を値として設定しました。
Azure 設定画面
🎉 完成したもの
これらを乗り越え、無事に掲示板が稼働しました。
実際の掲示板画面
まとめ
💰 運用コスト
完全無料で運用できています。
- Azure Static Web Apps: 個人利用は無料(月100GBまでの帯域幅)
- Azure Functions (Managed): SWAに統合されているため追加料金なし
- Cosmos DB: Free Tier(月間 1000 RU/s、25GB ストレージ)を適用
個人ブログレベルのアクセス数であれば、無料枠で十分に収まります。むしろWordPress + レンタルサーバー時代(月額1000円程度)からのコストダウンに成功しました。
🚀 今回の構成のメリット
-
インフラ管理不要
サーバーのメンテナンス、セキュリティパッチ適用、スケーリング設定など、すべてAzureにお任せできます。 -
高速な静的配信 + 必要な機能だけ動的に
Hugo生成の静的ファイルはCDNで高速配信され、掲示板などの動的機能だけAPIで補完する「いいとこ取り」構成です。 -
型安全な開発体験
C#の強い型システムにより、開発時にエラーを早期発見でき、リファクタリングも安心して行えます。 -
GitOps による自動デプロイ
GitHubにPushするだけで自動的にビルド・デプロイされるため、手動アップロードの手間がありません。
🛠 今後の拡張案
現在は最小構成ですが、以下のような機能拡張も可能です。
-
認証機能の追加
Azure AD B2C や GitHub OAuth などを統合し、ログインユーザー限定機能を実装 -
管理者機能
不適切な投稿を削除できる管理画面を追加(Azure Static Web Apps のルールベース認証と組み合わせる) -
通知機能
Azure Functions の Timer Trigger で定期的に新着投稿をチェックし、メール通知やSlack連携 -
画像アップロード
Azure Blob Storage と組み合わせて画像付き投稿を実装 -
全文検索
Azure Cognitive Search を統合し、過去の投稿を高速検索
📝 学びと所感
「静的サイトだから動的なことはできない」と諦めず、Azure Functionsを組み合わせることで、WordPress時代以上の自由度を手に入れることができました。
特に、デプロイ時のトラブルシューティング(ファイアウォール設定や環境変数の罠)は学びが多く、本番環境ならではの設定の重要性を再認識しました。
技術選定の際、「できるだけサーバーを触りたくない」「でも機能は妥協したくない」という矛盾した要求に対し、サーバーレスアーキテクチャは最適解の一つだと感じています。
皆さんもぜひ、掲示板に遊びに来て書き込みを残していってください!
ホームページの掲示板リンク
質問や改善案があれば、どんどん投稿していただけると嬉しいです。