はじめに

ブログを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)

まずはデータの保存先を作ります。ここでの設定が「無料」で運用するためのキモです。

  1. Azure Portal で 「Azure Cosmos DB for NoSQL」 を作成します。
  2. 容量モード: ここで必ず 「プロビジョニングされたスループット」 を選択し、「Free レベル割引の適用」を「適用」 にします。
  • ※これを忘れると課金が発生するので注意!
    Azure 設定画面

    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 CodeAzure Functions 拡張機能 を使って開発します。

プロジェクト作成

VS Codeのコマンドパレット (F1) から Azure Functions: Create New Project を実行します。

  1. フォルダ: Hugoプロジェクト直下に api フォルダを新規作成して選択
  2. 言語: C#
  3. ランタイム: .NET 8 Isolated
  4. テンプレート: HTTP trigger
  5. 認証レベル: Anonymous

必要なパッケージ

Cosmos DB を簡単に扱うため、NuGetパッケージを追加します。

dotnet add package Microsoft.Azure.Functions.Worker.Extensions.CosmosDB

実装コード (BbsFunctions.cs)

Input BindingOutput 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 設定画面

Azure 設定画面

ハマりポイント②:接続文字列の設定漏れ

ファイアウォールを通してもまだエラー。 原因は 環境変数の設定 です。ローカル開発用の local.settings.json はGitにコミットされないため、本番環境には鍵の情報がありません。

解決策: Azure Portal の Static Web Apps の「構成」→「アプリケーション設定」から、環境変数 CosmosDbConnectionString を手動で追加し、先ほどコピーしたCosmos DBのプライマリ接続文字列を値として設定しました。

Azure 設定画面

Azure 設定画面


🎉 完成したもの

これらを乗り越え、無事に掲示板が稼働しました。

実際の掲示板画面

実際の掲示板画面

こちらのURLから実際の動作も確認することができます!

ホームページの掲示板リンク


まとめ

💰 運用コスト

完全無料で運用できています。

  • Azure Static Web Apps: 個人利用は無料(月100GBまでの帯域幅)
  • Azure Functions (Managed): SWAに統合されているため追加料金なし
  • Cosmos DB: Free Tier(月間 1000 RU/s、25GB ストレージ)を適用

個人ブログレベルのアクセス数であれば、無料枠で十分に収まります。むしろWordPress + レンタルサーバー時代(月額1000円程度)からのコストダウンに成功しました。

🚀 今回の構成のメリット

  1. インフラ管理不要
    サーバーのメンテナンス、セキュリティパッチ適用、スケーリング設定など、すべてAzureにお任せできます。

  2. 高速な静的配信 + 必要な機能だけ動的に
    Hugo生成の静的ファイルはCDNで高速配信され、掲示板などの動的機能だけAPIで補完する「いいとこ取り」構成です。

  3. 型安全な開発体験
    C#の強い型システムにより、開発時にエラーを早期発見でき、リファクタリングも安心して行えます。

  4. 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時代以上の自由度を手に入れることができました。

特に、デプロイ時のトラブルシューティング(ファイアウォール設定や環境変数の罠)は学びが多く、本番環境ならではの設定の重要性を再認識しました。

技術選定の際、「できるだけサーバーを触りたくない」「でも機能は妥協したくない」という矛盾した要求に対し、サーバーレスアーキテクチャは最適解の一つだと感じています。

皆さんもぜひ、掲示板に遊びに来て書き込みを残していってください!
ホームページの掲示板リンク

質問や改善案があれば、どんどん投稿していただけると嬉しいです。