1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

自サイトに「古の掲示板」をAzureクラウド(SWA + Functions + Cosmos DB)で実装した話

Posted at

はじめに

個人ブログをWordPressから静的サイトジェネレーターの Hugo に移行し、ホスティング環境として Azure Static Web Apps を採用しました。
爆速で快適になった反面、コメント欄などの「動的機能」が失われてしまうのが静的サイトの宿命です。

Disqusなどの外部サービスを貼れば一瞬で解決するのですが、「せっかくAzure Static Web Appsを使っているのだから、同サービスの無料枠をフル活用して自作しよう」 と思い立ちました。

どうせ作るなら、最新のサーバーレスアーキテクチャを使って、あえて平成初期のような「古(いにしえ)の掲示板」 を錬成してみることにしました。

作ったもの

見た目はレトロ、中身はモダン。そんな掲示板です。

image.png

実際の動作デモ(筆者のブログ)
👉 https://electwork.net/bbs/

技術スタックとアーキテクチャ

個人開発の強い味方、Azureの 永久無料枠(Free Tier) を組み合わせた「お財布に優しい」構成です。

  • Frontend: Hugo

  • Hosting: Azure Static Web Apps (Free Plan)

  • Backend: Azure Functions (C# / .NET 8)

  • Static Web Appsに内包される「Managed Functions」を使用

  • Database: Azure Cosmos DB for NoSQL (Free Tier)

  • 最初の1000RU/sと25GBストレージが無料

なぜ「Managed Functions」なのか?

通常、Azure Functionsを利用する場合は独立した「関数アプリ」リソースを作成する必要がありますが、今回はAzure Static Web Apps (SWA) に統合されている Managed Functions 機能を利用しました。

これには明確なメリットがあります。

  1. CORS設定が不要: SWAの仕様上、フロントエンドとAPIが同一ドメイン(/api)として扱われるため、面倒なCORS(Cross-Origin Resource Sharing)の設定を一切行う必要がありません。
  2. デプロイの一元化: GitHub Actions のワークフロー1つ回すだけで、HugoのビルドとC# APIのデプロイが同時に完了します。

システム構成図

ユーザーがブラウザでアクセスすると、静的コンテンツはSWAから配信され、書き込み等のAPIリクエストはSWA内のAzure Functionsを経由してCosmos DBにアクセスします。

azure.jpg

データベースのコスト設計 (Cosmos DB)

「Cosmos DBは高い」というイメージがあるかもしれませんが、Free Tier を適用することで 毎月 1000 RU/s (Request Units) までは無料になります。

今回のようなテキストベースの掲示板の場合、1回の書き込みや読み込みで消費するのは 約5〜10 RU 程度です。
単純計算で秒間100回以上のアクセスがあっても無料枠に収まります。個人ブログの掲示板としては十分すぎるスペックですし、仮にバズって上限を超えてもスロットリング(429エラー)されるだけで、勝手に課金される心配がないのも安心ポイントです。

実装のポイント:C# (Input/Output Binding)

バックエンドは C# (.NET 8 Isolated Worker) で実装しました。
Azure Functionsの強力な機能である Binding(バインディング) を活用することで、DB接続のボイラープレートコードを一切書かずに 実装できます。

以下は「投稿を保存する」関数の全貌です。

bbsfunctions.cs
// 1. 投稿データを保存するAPI (POST)
[Function("CreatePost")]
[CosmosDBOutput(
    databaseName: "BBSDB",
    containerName: "Posts",
    Connection = "CosmosDbConnectionString", // 環境変数から接続文字列を取得
    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";

    // 戻り値のオブジェクトが、Output Bindingによって勝手にDBに保存される
    return post; 
}

SQLの INSERT 文も、Cosmos SDKの CreateItemAsync すら書いていません。
[CosmosDBOutput] 属性をつけるだけで、関数の戻り値 (post) が自動的にJSONとしてCosmos DBに格納されます。これがサーバーレス開発の醍醐味です。

⚠️ C#実装のハマりポイント(JSONプロパティ名)

地味ながら最大のトラップだったのが 「JSONプロパティ名のケーシング(大文字・小文字)問題」 です。

C#のコーディング規約ではプロパティ名は PascalCase(例: Category)が基本ですが、Cosmos DB側はパーティションキーとして camelCase(例: /category)を期待していました。
そのまま保存すると、Cosmos DB側で「パーティションキーが見つからない」というエラーが発生してしまいます。

これを解決するために、System.Text.Json.Serialization[JsonPropertyName] 属性を使用して、明示的にマッピングを行いました。

postitem.cs
public class PostItem
{
    // C#の世界では PascalCase だが、JSON (DB) には camelCase で渡す
    [JsonPropertyName("category")]
    public string Category { get; set; } = "general";
    
    [JsonPropertyName("message")]
    public string Message { get; set; }
    // ...
}

これにより、C#らしさを保ちつつ、DB側の要件を満たすことができました。

開発環境 (Developer Experience)

「サーバーレスはクラウドに上げないと確認できない」と思われがちですが、Azure Functions Core Tools を使うことで、ローカル環境でも快適に開発できます。

VS Codeで F5 を押せばローカルサーバーが立ち上がり、ブレークポイントを貼ってステップ実行も可能です。
今回はDBのみクラウド上のCosmos DBに接続しましたが、この開発体験(DX)の良さもC# × Azure Functionsの魅力だと感じました。

💣 ハマりポイント(デプロイ後のトラブル)

ローカル環境では完璧に動作したのに、Azureへデプロイした瞬間に動かなくなる……。
クラウド開発あるあるですが、今回遭遇した2つの罠と解決策を共有します。

1. HTTP 500エラー (ファイアウォール)

デプロイ後、APIを叩くと 500 Internal Server Error が発生しました。
ログを確認すると、接続拒否のエラーが出ています。

原因:
Cosmos DBのファイアウォール設定で、開発時の自宅IPアドレスのみ を許可していたためでした。
デプロイ後は、Azureのデータセンター内にある関数アプリ(SWA Managed Functions)からアクセスが来るため、これを許可する必要があります。

解決策:
Cosmos DBの「ネットワーク」設定で、「Azure データセンター内からの接続を許可する」 をONにします。
(許可リストにIP 0.0.0.0 が追加されます)
config.png

2. 環境変数の設定漏れ

ファイアウォールを通してもエラーが解消されません。
原因:
ローカル開発用の設定ファイル local.settings.json は、セキュリティ上Gitにはコミットされず、サーバーにもアップロードされません。そのため、本番環境には「DB接続文字列」が存在しない状態でした。

解決策:
Azure Portal の Static Web Apps の「環境変数」メニューから、CosmosDbConnectionString を手動で追加しました。

config_st.png

まとめ

コストと運用

  • 初期費用・維持費: 0円
  • 運用: サーバーレスのため、OSのアップデートやスケーリング管理は不要

所感

「静的サイトだから動的な機能は諦める」のではなく、Managed Functions を使うことで、バックエンドサーバーを別途立てることなく、リポジトリ一つでAPIまで実装できました。

特にC#を用いた開発は型安全で安心感があり、バインディング機能によってロジックに集中できるのが大きなメリットです。

今回は「掲示板」というレトロな題材を選びましたが、この構成は「問い合わせフォーム」「いいねボタン」「簡易アクセスカウンター」など、あらゆる動的機能に応用可能です。

もしよかったら、動作確認がてら私のサイトの掲示板に足跡(カキコ)を残していってください。
👉 https://electwork.net/bbs/

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?